单元测试基础 #
测试是保证代码质量的重要手段。Go语言内置了轻量级的测试框架,通过 testing 包可以轻松编写和运行测试。
测试基础 #
测试文件命名 #
测试文件以 _test.go 结尾,与被测试文件放在同一目录:
text
myapp/
├── calculator.go
└── calculator_test.go
第一个测试 #
go
package myapp
import "testing"
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
运行测试 #
bash
go test # 运行当前包的测试
go test -v # 详细输出
go test ./... # 运行所有包的测试
go test -run TestAdd # 运行特定测试
测试函数 #
测试函数签名 #
测试函数必须以 Test 开头,参数为 *testing.T:
go
package myapp
import "testing"
func TestFunctionName(t *testing.T) {
}
func TestAnotherFunction(t *testing.T) {
}
报告失败 #
go
package myapp
import "testing"
func TestExample(t *testing.T) {
got := 2 + 2
want := 4
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestFatal(t *testing.T) {
if true == false {
t.Fatal("这个错误会立即终止测试")
}
}
失败报告方法:
| 方法 | 说明 |
|---|---|
t.Error() |
记录错误,继续执行 |
t.Errorf() |
格式化记录错误,继续执行 |
t.Fatal() |
记录错误,立即终止 |
t.Fatalf() |
格式化记录错误,立即终止 |
t.Skip() |
跳过当前测试 |
t.Skipf() |
格式化跳过当前测试 |
表格驱动测试 #
表格驱动测试是Go推荐的测试模式,可以方便地测试多种情况:
go
package myapp
import "testing"
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"正数相加", 2, 3, 5},
{"负数相加", -2, -3, -5},
{"正负混合", 2, -3, -1},
{"零值测试", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.expected)
}
})
}
}
运行子测试 #
bash
go test -v -run TestAdd/正数相加
测试辅助函数 #
使用 t.Helper() #
标记辅助函数,使错误报告指向正确的行号:
go
package myapp
import "testing"
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestWithHelper(t *testing.T) {
assertEqual(t, 2+2, 4)
assertEqual(t, 3+3, 6)
}
测试清理 #
go
package myapp
import "testing"
func TestWithCleanup(t *testing.T) {
t.Cleanup(func() {
t.Log("清理资源")
})
t.Log("执行测试")
}
测试初始化 #
TestMain #
TestMain 用于测试前后的初始化和清理:
go
package myapp
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
os.Setenv("TEST_MODE", "true")
code := m.Run()
os.Unsetenv("TEST_MODE")
os.Exit(code)
}
测试覆盖率 #
生成覆盖率报告 #
bash
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
代码示例 #
go
package myapp
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
go
package myapp
import "testing"
func TestAbs(t *testing.T) {
tests := []struct {
input int
expected int
}{
{5, 5},
{-5, 5},
{0, 0},
}
for _, tt := range tests {
got := Abs(tt.input)
if got != tt.expected {
t.Errorf("Abs(%d) = %d; want %d",
tt.input, got, tt.expected)
}
}
}
测试浮点数 #
go
package myapp
import (
"math"
"testing"
)
func TestFloatEqual(t *testing.T) {
got := 0.1 + 0.2
want := 0.3
if math.Abs(got-want) > 1e-9 {
t.Errorf("got %f, want %f", got, want)
}
}
测试错误处理 #
go
package myapp
import (
"errors"
"testing"
)
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
t.Run("正常除法", func(t *testing.T) {
got, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != 5 {
t.Errorf("got %f, want 5", got)
}
})
t.Run("除零错误", func(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("expected error but got nil")
}
})
}
临时目录和文件 #
go
package myapp
import (
"os"
"path/filepath"
"testing"
)
func TestWithTempDir(t *testing.T) {
tempDir := t.TempDir()
filename := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(filename, []byte("test content"), 0644)
if err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
if string(content) != "test content" {
t.Errorf("unexpected content: %s", content)
}
}
实用示例:测试结构体方法 #
go
package myapp
import "testing"
type Calculator struct{}
func (c *Calculator) Add(a, b int) int {
return a + b
}
func (c *Calculator) Subtract(a, b int) int {
return a - b
}
func (c *Calculator) Multiply(a, b int) int {
return a * b
}
func (c *Calculator) Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivisionByZero
}
return a / b, nil
}
var ErrDivisionByZero = errors.New("division by zero")
func TestCalculator(t *testing.T) {
calc := &Calculator{}
t.Run("Add", func(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3},
{-1, 1, 0},
{0, 0, 0},
}
for _, tt := range tests {
got := calc.Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.want)
}
}
})
t.Run("Divide", func(t *testing.T) {
t.Run("正常除法", func(t *testing.T) {
got, err := calc.Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != 5 {
t.Errorf("got %d, want 5", got)
}
})
t.Run("除零错误", func(t *testing.T) {
_, err := calc.Divide(10, 0)
if err != ErrDivisionByZero {
t.Errorf("expected ErrDivisionByZero, got %v", err)
}
})
})
}
小结 #
| 命令 | 说明 |
|---|---|
go test |
运行测试 |
go test -v |
详细输出 |
go test -run |
运行匹配的测试 |
go test -cover |
显示覆盖率 |
go test -coverprofile |
生成覆盖率报告 |
单元测试是保证代码质量的基础,良好的测试习惯可以大大减少bug数量,提高代码可维护性。