Go · #go#testing#benchmark#mock

Go测试策略:从单元测试到集成测试

2023.10.11 Go 12 min 4.7k
// 目录 · contents

引言

Go语言从诞生之初就将测试作为一等公民。标准库中的testing包提供了完整的测试、基准测试和示例测试支持,go test命令则提供了开箱即用的测试执行环境。这种内置的测试文化使得Go项目通常拥有较高的测试覆盖率。

本文将系统介绍Go的测试策略,从基础的单元测试出发,覆盖表驱动测试、Mock技术、HTTP测试、基准测试、模糊测试和集成测试等主题。

测试金字塔

graph TB
    subgraph "测试金字塔"
        E2E["E2E测试<br/>少量,慢,高成本"]
        INT["集成测试<br/>适量,中等速度"]
        UNIT["单元测试<br/>大量,快速,低成本"]
    end

    E2E --> INT --> UNIT

    style E2E fill:#f99,stroke:#333
    style INT fill:#ff9,stroke:#333
    style UNIT fill:#9f9,stroke:#333

基础单元测试

测试文件命名约定

Go的测试文件必须以_test.go结尾,与被测代码放在同一包中:

1
2
3
mypackage/
calculator.go # 被测代码
calculator_test.go # 测试代码

基本测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// calculator.go
package calculator

import (
"errors"
"math"
)

var (
ErrDivisionByZero = errors.New("division by zero")
ErrNegativeSqrt = errors.New("cannot take square root of negative number")
)

func Add(a, b float64) float64 {
return a + b
}

func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, ErrDivisionByZero
}
return a / b, nil
}

func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt
}
return math.Sqrt(x), nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// calculator_test.go
package calculator

import (
"math"
"testing"
)

func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %f, want 5", result)
}
}

func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5 {
t.Errorf("Divide(10, 2) = %f, want 5", result)
}
}

func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Fatal("expected error, got nil")
}
if err != ErrDivisionByZero {
t.Errorf("expected ErrDivisionByZero, got %v", err)
}
}

表驱动测试(Table-Driven Tests)

表驱动测试是Go社区推崇的测试风格,它将测试用例组织为表格,每行是一个独立的测试场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package calculator

import (
"math"
"testing"
)

func TestAdd_TableDriven(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed signs", -2, 3, 1},
{"zeros", 0, 0, 0},
{"large numbers", 1e18, 1e18, 2e18},
{"decimals", 0.1, 0.2, 0.3},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
// 浮点数比较使用误差范围
if math.Abs(result-tt.expected) > 1e-9 {
t.Errorf("Add(%f, %f) = %f, want %f",
tt.a, tt.b, result, tt.expected)
}
})
}
}

func TestDivide_TableDriven(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
expectErr error
}{
{"normal division", 10, 2, 5, nil},
{"division by zero", 10, 0, 0, ErrDivisionByZero},
{"negative result", -10, 2, -5, nil},
{"fractional result", 1, 3, 0.333333, nil},
{"divide zero", 0, 5, 0, nil},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)

if tt.expectErr != nil {
if err != tt.expectErr {
t.Errorf("expected error %v, got %v", tt.expectErr, err)
}
return
}

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if math.Abs(result-tt.expected) > 1e-4 {
t.Errorf("Divide(%f, %f) = %f, want %f",
tt.a, tt.b, result, tt.expected)
}
})
}
}

func TestSqrt_TableDriven(t *testing.T) {
tests := []struct {
name string
input float64
expected float64
expectErr error
}{
{"positive number", 16, 4, nil},
{"zero", 0, 0, nil},
{"negative number", -1, 0, ErrNegativeSqrt},
{"non-perfect square", 2, math.Sqrt2, nil},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Sqrt(tt.input)
if tt.expectErr != nil {
if err != tt.expectErr {
t.Errorf("expected error %v, got %v", tt.expectErr, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if math.Abs(result-tt.expected) > 1e-9 {
t.Errorf("Sqrt(%f) = %f, want %f", tt.input, result, tt.expected)
}
})
}
}

使用testify增强断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package calculator

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestWithTestify(t *testing.T) {
// assert: 失败后继续执行后续断言
assert.Equal(t, 5.0, Add(2, 3), "addition should work")
assert.InDelta(t, 0.3, Add(0.1, 0.2), 1e-9, "floating point addition")

// require: 失败后立即停止当前测试
result, err := Divide(10, 2)
require.NoError(t, err, "divide should not error")
require.Equal(t, 5.0, result)

// 测试错误
_, err = Divide(10, 0)
require.ErrorIs(t, err, ErrDivisionByZero)
}

Mock技术

接口Mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// user_service.go
package service

import "context"

type User struct {
ID string
Name string
Email string
}

// 定义接口(在消费方定义)
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}

type UserService struct {
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}

func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
if id == "" {
return nil, ErrInvalidID
}
return s.repo.FindByID(ctx, id)
}

func (s *UserService) CreateUser(ctx context.Context, name, email string) (*User, error) {
if name == "" || email == "" {
return nil, ErrInvalidInput
}

user := &User{
ID: generateID(),
Name: name,
Email: email,
}

if err := s.repo.Save(ctx, user); err != nil {
return nil, err
}
return user, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// user_service_test.go
package service

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

// 手动Mock
type MockUserRepository struct {
mock.Mock
}

func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}

func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
args := m.Called(ctx, user)
return args.Error(0)
}

func (m *MockUserRepository) Delete(ctx context.Context, id string) error {
args := m.Called(ctx, id)
return args.Error(0)
}

func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID string
mockSetup func(*MockUserRepository)
expected *User
expectErr error
}{
{
name: "success",
userID: "user-1",
mockSetup: func(repo *MockUserRepository) {
repo.On("FindByID", mock.Anything, "user-1").
Return(&User{ID: "user-1", Name: "Alice", Email: "[email protected]"}, nil)
},
expected: &User{ID: "user-1", Name: "Alice", Email: "[email protected]"},
},
{
name: "empty id",
userID: "",
mockSetup: func(repo *MockUserRepository) {},
expectErr: ErrInvalidID,
},
{
name: "not found",
userID: "nonexistent",
mockSetup: func(repo *MockUserRepository) {
repo.On("FindByID", mock.Anything, "nonexistent").
Return(nil, errors.New("user not found"))
},
expectErr: errors.New("user not found"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := new(MockUserRepository)
tt.mockSetup(mockRepo)

svc := NewUserService(mockRepo)
user, err := svc.GetUser(context.Background(), tt.userID)

if tt.expectErr != nil {
require.Error(t, err)
assert.Equal(t, tt.expectErr.Error(), err.Error())
return
}

require.NoError(t, err)
assert.Equal(t, tt.expected, user)
mockRepo.AssertExpectations(t)
})
}
}

使用mockgen生成Mock

1
2
3
4
5
# 安装mockgen
go install go.uber.org/mock/mockgen@latest

# 从接口生成mock
mockgen -source=user_service.go -destination=mocks/mock_repository.go -package=mocks

HTTP测试

Go标准库的net/http/httptest包提供了优秀的HTTP测试支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// handler.go
package api

import (
"encoding/json"
"net/http"
)

type UserHandler struct {
service UserServiceInterface
}

type UserServiceInterface interface {
GetUser(id string) (*User, error)
CreateUser(name, email string) (*User, error)
}

type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}

user, err := h.service.GetUser(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

user, err := h.service.CreateUser(req.Name, req.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// handler_test.go
package api

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Mock service for testing
type mockUserService struct {
getUser func(id string) (*User, error)
createUser func(name, email string) (*User, error)
}

func (m *mockUserService) GetUser(id string) (*User, error) {
return m.getUser(id)
}

func (m *mockUserService) CreateUser(name, email string) (*User, error) {
return m.createUser(name, email)
}

func TestGetUser(t *testing.T) {
tests := []struct {
name string
queryID string
mockReturn *User
mockErr error
expectedStatus int
expectedBody *User
}{
{
name: "success",
queryID: "1",
mockReturn: &User{ID: "1", Name: "Alice", Email: "[email protected]"},
expectedStatus: http.StatusOK,
expectedBody: &User{ID: "1", Name: "Alice", Email: "[email protected]"},
},
{
name: "missing id",
queryID: "",
expectedStatus: http.StatusBadRequest,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := &mockUserService{
getUser: func(id string) (*User, error) {
return tt.mockReturn, tt.mockErr
},
}
handler := &UserHandler{service: svc}

req := httptest.NewRequest("GET", "/users?id="+tt.queryID, nil)
rec := httptest.NewRecorder()

handler.GetUser(rec, req)

assert.Equal(t, tt.expectedStatus, rec.Code)

if tt.expectedBody != nil {
var body User
err := json.NewDecoder(rec.Body).Decode(&body)
require.NoError(t, err)
assert.Equal(t, tt.expectedBody, &body)
}
})
}
}

func TestCreateUser(t *testing.T) {
svc := &mockUserService{
createUser: func(name, email string) (*User, error) {
return &User{ID: "new-1", Name: name, Email: email}, nil
},
}
handler := &UserHandler{service: svc}

body := bytes.NewBufferString(`{"name":"Bob","email":"[email protected]"}`)
req := httptest.NewRequest("POST", "/users", body)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()

handler.CreateUser(rec, req)

assert.Equal(t, http.StatusCreated, rec.Code)

var user User
err := json.NewDecoder(rec.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "Bob", user.Name)
assert.Equal(t, "[email protected]", user.Email)
}

// 使用httptest.Server进行端到端测试
func TestHTTPServer(t *testing.T) {
svc := &mockUserService{
getUser: func(id string) (*User, error) {
return &User{ID: id, Name: "Test User"}, nil
},
}
handler := &UserHandler{service: svc}

mux := http.NewServeMux()
mux.HandleFunc("/users", handler.GetUser)

server := httptest.NewServer(mux)
defer server.Close()

// 使用真实的HTTP客户端发送请求
resp, err := http.Get(server.URL + "/users?id=1")
require.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)
}

基准测试(Benchmark)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// calculator_bench_test.go
package calculator

import (
"testing"
)

func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(3.14, 2.71)
}
}

func BenchmarkDivide(b *testing.B) {
for i := 0; i < b.N; i++ {
Divide(100, 7)
}
}

// 带子基准测试
func BenchmarkSqrt(b *testing.B) {
inputs := []struct {
name string
value float64
}{
{"small", 4},
{"medium", 1000},
{"large", 1e18},
}

for _, input := range inputs {
b.Run(input.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
Sqrt(input.value)
}
})
}
}

// 基准测试内存分配
func BenchmarkSliceAppend(b *testing.B) {
b.Run("no_prealloc", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := []int{}
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
})

b.Run("prealloc", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := make([]int, 0, 1000)
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
})
}

运行基准测试:

1
2
3
4
5
6
7
8
9
10
# 运行所有基准测试
go test -bench=. -benchmem ./...

# 运行特定基准测试
go test -bench=BenchmarkAdd -benchmem -count=5

# 输出示例:
# BenchmarkAdd-8 1000000000 0.3186 ns/op 0 B/op 0 allocs/op
# BenchmarkSliceAppend/no_prealloc-8 72735 16421 ns/op 25208 B/op 12 allocs/op
# BenchmarkSliceAppend/prealloc-8 233076 5124 ns/op 8192 B/op 1 allocs/op
graph LR
    subgraph "基准测试流程"
        A["go test -bench"] --> B["warm up"]
        B --> C["动态调整 b.N"]
        C --> D["运行 b.N 次"]
        D --> E["计算 ns/op, B/op"]
        E --> F["输出结果"]
    end

模糊测试(Fuzz Testing,Go 1.18+)

模糊测试自动生成随机输入来发现边界情况和潜在的bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package calculator

import (
"math"
"testing"
)

func FuzzDivide(f *testing.F) {
// 添加种子语料
f.Add(10.0, 2.0)
f.Add(0.0, 1.0)
f.Add(-5.0, 3.0)
f.Add(1e18, 1e-18)

f.Fuzz(func(t *testing.T, a, b float64) {
result, err := Divide(a, b)

if b == 0 {
if err != ErrDivisionByZero {
t.Errorf("Divide(%f, 0) should return ErrDivisionByZero", a)
}
return
}

if err != nil {
t.Errorf("unexpected error for Divide(%f, %f): %v", a, b, err)
return
}

// 验证结果的正确性
if !math.IsNaN(result) && !math.IsInf(result, 0) {
reconstructed := result * b
if math.Abs(reconstructed-a) > 1e-6*math.Abs(a)+1e-10 {
t.Errorf("Divide(%f, %f) = %f, but %f * %f = %f",
a, b, result, result, b, reconstructed)
}
}
})
}

func FuzzSqrt(f *testing.F) {
f.Add(4.0)
f.Add(0.0)
f.Add(2.0)

f.Fuzz(func(t *testing.T, x float64) {
result, err := Sqrt(x)

if x < 0 {
if err != ErrNegativeSqrt {
t.Errorf("Sqrt(%f) should return ErrNegativeSqrt", x)
}
return
}

if err != nil {
t.Errorf("unexpected error for Sqrt(%f): %v", x, err)
return
}

// 验证 result^2 ≈ x
squared := result * result
if math.Abs(squared-x) > 1e-9*math.Abs(x)+1e-15 {
t.Errorf("Sqrt(%f) = %f, but %f^2 = %f", x, result, result, squared)
}
})
}
1
2
3
4
# 运行模糊测试(持续运行直到发现bug或手动停止)
go test -fuzz=FuzzDivide -fuzztime=30s

# 发现的crash case会保存在 testdata/fuzz/ 目录下

测试覆盖率

1
2
3
4
5
6
7
8
9
10
11
12
13
# 生成覆盖率报告
go test -coverprofile=coverage.out ./...

# 查看覆盖率摘要
go tool cover -func=coverage.out

# 在浏览器中查看详细覆盖率(高亮显示覆盖/未覆盖的行)
go tool cover -html=coverage.out

# 设置最低覆盖率阈值
go test -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
awk -F'%' '{if ($1 < 80) exit 1}'
flowchart LR
    subgraph "测试覆盖率工作流"
        A["go test -cover"] --> B["生成 coverage.out"]
        B --> C["go tool cover -html"]
        C --> D["浏览器查看"]
        B --> E["go tool cover -func"]
        E --> F["终端摘要"]
    end

TestMain:测试生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package integration

import (
"database/sql"
"log"
"os"
"testing"
)

var testDB *sql.DB

// TestMain 控制整个测试文件的生命周期
func TestMain(m *testing.M) {
// Setup: 在所有测试之前执行
var err error
testDB, err = setupTestDatabase()
if err != nil {
log.Fatalf("failed to setup test database: %v", err)
}

// 运行所有测试
code := m.Run()

// Teardown: 在所有测试之后执行
teardownTestDatabase(testDB)

os.Exit(code)
}

func setupTestDatabase() (*sql.DB, error) {
db, err := sql.Open("postgres", os.Getenv("TEST_DATABASE_URL"))
if err != nil {
return nil, err
}
// 执行迁移等初始化操作
return db, nil
}

func teardownTestDatabase(db *sql.DB) {
if db != nil {
db.Close()
}
}

// t.Cleanup: 单个测试的清理
func TestWithCleanup(t *testing.T) {
// 创建临时资源
tmpFile := createTempFile(t)

// 注册清理函数(测试结束时自动执行,包括失败时)
t.Cleanup(func() {
os.Remove(tmpFile)
})

// 测试逻辑...
}

func createTempFile(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp("", "test-*")
if err != nil {
t.Fatal(err)
}
f.Close()
return f.Name()
}

性能考量与最佳实践

测试组织建议

graph TB
    subgraph "推荐的测试组织"
        A["unit tests<br/>*_test.go<br/>同包,无外部依赖"] --> B["integration tests<br/>*_integration_test.go<br/>使用build tags"]
        B --> C["e2e tests<br/>e2e/*_test.go<br/>独立包"]
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
//go:build integration

// 集成测试使用build tag隔离
package integration

import "testing"

func TestDatabaseIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
// 集成测试逻辑...
}
1
2
3
4
5
6
7
8
# 只运行单元测试(快速)
go test -short ./...

# 包含集成测试
go test -tags=integration ./...

# 并行运行测试
go test -parallel=4 ./...

测试辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
// testutil/helpers.go
package testutil

import "testing"

// RequireEqual 是一个简单的断言辅助函数
func RequireEqual(t *testing.T, expected, actual interface{}) {
t.Helper() // 标记为辅助函数,错误报告时显示调用者的位置
if expected != actual {
t.Fatalf("expected %v, got %v", expected, actual)
}
}

总结

Go的测试体系虽然简洁,但非常强大:

  1. 表驱动测试是Go测试的标准范式,结构清晰,易于维护和扩展
  2. 接口Mock配合testify的mock包,可以有效隔离外部依赖
  3. httptest提供了完整的HTTP测试支持,从单个handler到完整server
  4. 基准测试内置支持,可以精确测量性能和内存分配
  5. 模糊测试(Go 1.18+)自动发现边界情况
  6. 测试覆盖率工具帮助识别测试盲区

实践建议: - 优先编写表驱动测试 - 在消费方定义接口,便于Mock - 使用t.Helper()标记辅助函数 - 使用t.Cleanup()确保资源释放 - 用build tags隔离集成测试和单元测试 - 保持单元测试的快速执行(目标:总时间 < 10s)

作者 · authorzt
发布 · date2023-10-11
篇幅 · length4.7k 字 · 12 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论