Go测试教程展示了如何使用内置的测试包在Golang中进行测试。
$ go version go version go1.18.1 linux/amd64
我们使用Go版本1.18。
单元测试是一个软件测试分支,测试软件的各个部分。单元测试的目的是验证软件的每个单元是否按设计执行。单元是任何软件中最小的可测试部分。单元测试不同于集成测试,后者将软件应用程序的不同单元和模块作为一个整体进行测试。
Go包含一个内置包testing
用于进行测试。测试被写入以_test.go
结尾的文件中。函数名的形式为
func TestXxx(*testing.T)
其中Xxx
是要测试的函数的名称。
测试是通过gotest
命令开始的。该命令编译程序和测试源并运行测试二进制文件。Gotest查找名称与文件模式*_test.go
匹配的文件。最后会显示测试运行摘要。
Go测试文件可以包含测试函数、基准函数、模糊测试和示例函数。
Gotest可以在两种模式下运行:a)本地目录模式或b)包列表模式。当我们运行gotest
时启用本地目录模式包参数。在这种模式下,gotest编译在当前目录中找到的包源和测试,然后运行生成的测试二进制文件。在此模式下缓存被禁用。
包列表模式
在使用显式包名称运行命令gotest
时启用;例如,gotest.
、gotest./...
(目录树中的所有包)或gotestutils
。在此模式下,gotest
编译并测试命令行中列出的每个包。此外,它缓存成功的包测试结果,以避免不必要的重复运行测试
gotest-v
,其中-v
标志代表详细,打印出所有已执行的测试函数的名称及其执行时间。使用-coverage
选项运行测试代码覆盖率。我们可以使用-run
选项运行特定测试,我们在其中应用正则表达式定位函数名称。
进行简单测试
在第一个例子中,我们测试了两个简单的函数。
package main func hello() string { return "Hello there!" } func morning() string { return "Good morning!" }
这两个函数返回短文本消息。
package main import "testing" func TestHello(t *testing.T) { got := hello() want := "Hello there!" if got != want { t.Errorf("got %s, want %s", got, want) } } func TestMorning(t *testing.T) { got := morning() want := "Good morning!" if got != want { t.Errorf("got %s, want %s", got, want) } }
文件的名称是message_test.go
。
import "testing"
testing
包已导入。
func TestHello(t *testing.T) { got := hello() want := "Hello there!" if got != want { t.Errorf("got %s, want %s", got, want) } }
被测试的函数前面有Test
关键字。如果预期值和返回值不同,我们会写一条错误消息。
$ go test PASS ok com.zetcode/first 0.001s
我们使用gotest
命令运行测试。由于我们没有指定任何包名称,该工具会在当前工作目录中查找_test.go
文件。通过发现的文件,它会查找具有TestXxx(*testing.T)
签名的函数。
$ go test -v === RUN TestHello --- PASS: TestHello (0.00s) === RUN TestMorning --- PASS: TestMorning (0.00s) PASS ok com.zetcode/first 0.001s
要获取更多信息,我们使用-v
选项。
$ go test -v -run Hello === RUN TestHello --- PASS: TestHello (0.00s) PASS ok com.zetcode/first 0.001s $ go test -v -run Mor === RUN TestMorning --- PASS: TestMorning (0.00s) PASS ok com.zetcode/first 0.001s
我们通过将正则表达式模式传递给-run
选项来运行特定功能。
去测试算术函数
在下一个例子中,我们将测试四个算术函数。
package main func Add(x int, y int) int { return x + y } func Sub(x int, y int) int { return x - y } func Div(x float64, y float64) float64 { return x / y } func Mul(x int, y int) int { return x * y }
我们有加法、减法、除法和乘法函数。
package main import "testing" func TestAdd(t *testing.T) { x, y := 2, 3 want := 5 got := Add(x, y) if got != want { t.Errorf("got %d, want %d", got, want) } } func TestSub(t *testing.T) { x, y := 5, 3 want := 2 got := Sub(x, y) if got != want { t.Errorf("got %d, want %d", got, want) } } func TestDiv(t *testing.T) { x, y := 7., 2. want := 3.5 got := Div(x, y) if got != want { t.Errorf("got %f, want %f", got, want) } } func TestMul(t *testing.T) { x, y := 6, 5 want := 30 got := Mul(x, y) if got != want { t.Errorf("got %d, want %d", got, want) } }
在每个函数中,我们提供测试值和预期输出。
$ go test -v === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN TestSub --- PASS: TestSub (0.00s) === RUN TestDiv --- PASS: TestDiv (0.00s) === RUN TestMul --- PASS: TestMul (0.00s) PASS ok com.zetcode/math 0.001s
我们已经通过了所有四项测试。
$ go test -cover PASS coverage: 100.0% of statements ok com.zetcode/math 0.001s
-cover
选项提供有关测试覆盖了多少函数的信息。
$ go test -v -run "TestSub|TestMul" === RUN TestSub --- PASS: TestSub (0.00s) === RUN TestMul --- PASS: TestMul (0.00s) PASS ok com.zetcode/math 0.002s
使用管道运算符,我们选择两个特定的测试函数来运行。
Go表驱动测试
对于表驱动测试,我们有一个包含不同值和结果的表。测试工具迭代这些值并将它们传递给测试代码。这样我们就可以测试几种输入组合及其各自的输出。
这在其他语言中也称为参数化测试。
package main type Val interface { int | float64 } func Add[T Val](x T, y T) T { return x + y } func Sub[T Val](x T, y T) T { return x - y } func Div[T Val](x T, y T) T { return x / y } func Mul[T Val](x T, y T) T { return x * y }
使用泛型;我们可以传递整数和浮点值作为参数。
package main import "testing" type TestCase[T Val] struct { arg1 T arg2 T want T } func TestAdd(t *testing.T) { cases := []TestCase[int]{ {2, 3, 5}, {5, 5, 10}, {-7, 6, -1}, } for _, tc := range cases { got := Add(tc.arg1, tc.arg2) if tc.want != got { t.Errorf("Expected '%d', but got '%d'", tc.want, got) } } } func TestSub(t *testing.T) { cases := []TestCase[int]{ {2, 3, -1}, {5, 5, 0}, {-7, -3, -4}, } for _, tc := range cases { got := Sub(tc.arg1, tc.arg2) if tc.want != got { t.Errorf("Expected '%d', but got '%d'", tc.want, got) } } } func TestDiv(t *testing.T) { cases := []TestCase[int]{ {6., 3., 2.}, {5., 5., 1.}, {-10., 2., -5.}, } for _, tc := range cases { got := Div(tc.arg1, tc.arg2) if tc.want != got { t.Errorf("Expected '%d', but got '%d'", tc.want, got) } } } func TestMul(t *testing.T) { cases := []TestCase[int]{ {7, 3, 21}, {5, 5, 25}, {-1, 6, -6}, } for _, tc := range cases { got := Mul(tc.arg1, tc.arg2) if tc.want != got { t.Errorf("Expected '%d', but got '%d'", tc.want, got) } } }
我们的测试现在每个都有三个测试用例。
type TestCase[T Val] struct { arg1 T arg2 T want T }
我们创建一个TestCase
类型,其中包含输入值和预期输出的字段。
func TestAdd(t *testing.T) { cases := []TestCase[int]{ {2, 3, 5}, {5, 5, 10}, {-7, 6, -1}, } for _, tc := range cases { got := Add(tc.arg1, tc.arg2) if tc.want != got { t.Errorf("Expected '%d', but got '%d'", tc.want, got) } } }
我们有一个包含三个测试用例的片段。我们遍历切片并为每种情况调用测试函数。
$ go test -v === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN TestSub --- PASS: TestSub (0.00s) === RUN TestDiv --- PASS: TestDiv (0.00s) === RUN TestMul --- PASS: TestMul (0.00s) PASS ok com.zetcode/tables 0.001s
Go测试示例函数
可以添加用于运行一些基本测试和文档的示例函数。示例测试函数以示例词开头。
func ExampleHello() { fmt.Println("hello") // Output: hello }
运行函数并将输出与输出字后面的值进行比较。
package main func Add(x int, y int) int { return x + y }
我们有一个简单的Add
函数。
package main import ( "fmt" "testing" ) func TestAdd(t *testing.T) { x, y := 2, 3 want := 5 got := Add(x, y) if got != want { t.Errorf("got %d, want %d", got, want) } } func ExampleAdd() { fmt.Println(Add(10, 6)) // Output: 16 }
测试文件包含TestAdd
函数和ExampleAdd
示例测试函数。
$ go test -v === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN ExampleAdd --- PASS: ExampleAdd (0.00s) PASS ok com.zetcode/example 0.002s
去httptest
httptest
包包含用于测试HTTP流量的实用程序。
ResponseRecorder
是http.ResponseWriter
的一个实现,它记录它的变化以供以后在测试中检查。
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", HelloHandler) log.Println("Listening...") log.Fatal(http.ListenAndServe(":8080", nil)) } func HelloHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "Hello, there\n") }
我们有一个带有一个HelloHandler
的简单HTTP服务器。
package main import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" ) func TestHelloHandler(t *testing.T) { want := "Hello there!" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, want) })) defer ts.Close() client := ts.Client() res, err := client.Get(ts.URL) if err != nil { t.Errorf("expected nil got %v", err) } data, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { t.Errorf("expected nil got %v", err) } got := strings.TrimSpace(string(data)) if string(got) != want { t.Errorf("got %s, want %s", got, want) } }
在TestHelloHandler
中,我们使用httptest.NewServer
启动测试服务器,并为HelloHandler
实现一个相同的处理程序。客户端生成请求,并将响应与预期输出进行比较。在函数结束时,服务器关闭。
在本教程中,我们使用内置的测试模块在Go中执行了测试。
列出所有Go教程。