Go 测试
最后修改时间 2024 年 4 月 11 日
在本教程中,我们将展示如何使用 Go 中内置的测试包进行测试。
单元测试 是软件测试的一个分支,用于测试软件的各个部分。单元测试的目的是验证软件的每个单元是否按设计执行。单元是任何软件中最小的可测试部分。单元测试与集成测试不同,集成测试是将软件应用程序的不同单元和模块作为一个整体进行测试。
Go 包含一个内置的 testing 包用于进行测试。测试是写在以 _test.go 结尾的文件中的。函数名遵循以下形式:
func TestXxx(*testing.T)
其中 Xxx 是要被测试的函数名。
测试通过 go test 命令启动。该命令会编译程序和测试源文件,然后运行测试二进制文件。Go test 会查找名称匹配 *_test.go 文件模式的文件。最后会显示测试运行的摘要。
Go 测试文件可以包含测试函数、基准测试函数、模糊测试和示例函数。
Go test 可以有两种模式运行:a) 本地目录模式或 b) 包列表模式。当我们运行不带任何包参数的 go test 时,会启用本地目录模式。在此模式下,go test 会编译当前目录中的包源文件和测试文件,然后运行生成的测试二进制文件。此模式下会禁用缓存。
当运行带有显式包名称的 go test 命令时,例如 go test .、go test ./...(目录树中的所有包)或 go test utils,会启用包列表模式。在此模式下,go test 会编译并测试命令行中列出的每个包。此外,它还会缓存成功的包测试结果,以避免不必要的重复运行测试。
go test -v,其中 -v 标志代表 verbose(详细),会打印出所有已执行的测试函数的名称及其执行时间。测试代码覆盖率通过 -coverage 选项运行。我们可以使用 -run 选项运行特定的测试,其中我们应用一个正则表达式来定位函数名称。
Go 简单测试
在第一个示例中,我们测试了两个简单的函数。
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
我们使用 go test 命令运行测试。由于我们没有指定任何包名称,该工具会在当前工作目录中查找 _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 选项传递正则表达式模式来运行特定的函数。
Go 测试算术函数
在下一个示例中,我们将测试四个算术函数。
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 示例函数测试
可以为运行一些基本测试和文档添加示例函数。示例测试函数以 Example 开头。
func ExampleHello() {
fmt.Println("hello")
// Output: hello
}
运行该函数,并将其输出与 Output 关键字后面的值进行比较。
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
Go 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 中进行了测试。