Files
cs-note/hzh/GolangStar/Go语言单测/Go语言单测.md
T

312 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, 单元测试, testing, Mock]
create time: 2026-06-07 15:30
---
# Go 单元测试
## 概述
单元测试是保障代码质量的基石。Go 内置了 `testing` 包和 `go test` 命令,配合丰富的第三方生态(GoConvey、testify、GoMock),形成了一套完整的测试体系。本文从基础到高级覆盖所有核心技能。
## 正文
### 为什么写测试?
> [!question] 💭 思考
> 你花了三天写完一个函数,提交前觉得"应该没问题"——但上线后第一个用户就遇到了 panic。如果写了单测呢?
单元测试的核心价值:
1. **防回归**:修改旧代码时自动验证已有功能未被破坏
2. **设计指导**:可测试的代码通常意味着清晰的接口边界
3. **文档作用**:测试用例是最准确的"这个函数应该怎么用"的说明
4. **信心保障**:重构时敢放手改,因为测试会捕获遗漏
> [!warning] ⚠️ 常见误区
> - "项目太急没时间写测试"——短期省下的时间会在后期 bug 修复中加倍偿还
> - "测试代码不需要维护"——烂测试比没测试更糟糕(假阳性让人失去信任)
> - "100% 覆盖率 = 好软件"——覆盖率为零的项目一定有问题,但 100% 不等于没有 bug
### 命名规范与目录结构
```
myproject/
├── gotest/
│ ├── example.go # 被测试代码
│ └── example_test.go # 测试代码(必须以 _test.go 结尾)
```
| 规则 | 说明 |
|------|------|
| 文件名 | `*_test.go` —— 只有这个后缀的文件才会被 `go test` 识别 |
| 测试函数 | `func TestXxx(t *testing.T)` — Xxx 首字母不能是小写 |
| 基准测试 | `func BenchmarkXxx(b *testing.B)` |
| 示例函数 | `func ExampleXxx()` — 带输出注释的示例 |
### 基础测试
#### 被测试代码
```go
package gotest
func Factorial(n int) int {
if n <= 0 {
return 1
}
return n * Factorial(n-1)
}
```
#### 测试代码
```go
func TestFactorial(t *testing.T) {
result := Factorial(5)
if result != 120 {
t.Errorf("Factorial(5) = %d; want 120", result)
}
}
```
运行:
```bash
$ go test -v
=== RUN TestFactorial
--- PASS: TestFactorial (0.00s)
PASS
```
### 表格驱动测试(Table-Driven Tests)
> [!tip] 💡 Go 测试的黄金模板
> 表格驱动测试是 Go 社区最推崇的测试模式——一条用例定义 + 一个循环覆盖,简洁且易扩展。
```go
func TestFactorial(t *testing.T) {
tests := []struct {
name string
input int
expected int
}{
{"zero", 0, 1},
{"one", 1, 1},
{"five", 5, 120},
{"ten", 10, 3628800},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // 子测试,可单独运行
result := Factorial(tt.input)
if result != tt.expected {
t.Errorf("Factorial(%d) = %d; want %d", tt.input, result, tt.expected)
}
})
}
}
```
运行结果:
```bash
=== RUN TestFactorial
=== RUN TestFactorial/zero
=== RUN TestFactorial/one
=== RUN TestFactorial/five
=== RUN TestFactorial/ten
--- PASS: TestFactorial (0.00s)
```
> [!tip] 💡 可复用的表格驱动模板
> ```go
> func TestName(t *testing.T) {
> type args struct { /* 参数结构 */ }
> tests := []struct {
> name string
> args args
> want 返回类型
> wantErr bool
> }{
> {"正常情况", args{...}, 期望值, false},
> {"边界情况", args{...}, 期望值, false},
> {"错误情况", args{...}, 期望值, true},
> }
> for _, tt := range tests {
> t.Run(tt.name, func(t *testing.T) {
> got, err := FunctionName(tt.args.xxx)
> if (err != nil) != tt.wantErr {
> t.Errorf("error state = %v", err)
> return
> }
> if got != tt.want {
> t.Errorf("got = %v, want %v", got, tt.want)
> }
> })
> }
> }
> ```
### GoConvey — BDD 风格测试
```go
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestSlicesEqual(t *testing.T) {
Convey("给定两个切片,比较它们是否相等", t, func() {
Convey("内容相同时", func() {
So(SlicesEqual([]int{1,2,3}, []int{1,2,3}), ShouldBeTrue)
})
Convey("长度不同时", func() {
So(SlicesEqual([]int{1,2}, []int{1,2,3}), ShouldBeFalse)
})
Convey("元素不同时报错", func() {
So(SlicesEqual([]int{1,3}, []int{1,2}), ShouldBeFalse)
})
})
}
```
> [!note] 📝 断言速查
> | 断言 | 含义 |
> |------|------|
> | `ShouldBeTrue / ShouldBeFalse` | 布尔值判断 |
> | `ShouldEqual / ShouldNotEqual` | 相等性判断 |
> | `ShouldBeNil / ShouldNotBeNil` | nil 检查 |
> | `ShouldContain / ShouldNotContain` | 集合包含检查 |
### Stub / Mock 框架
> [!question] 💭 思考
> 要测试一个调用数据库的函数,难道每次都要启动真实的数据库吗?
测试的核心原则:**隔离被测单元**。外部依赖(DB、网络、文件系统)应被替换为可控的替代品。
#### Stub vs Mock 的区别
| 概念 | 目的 | 能力 | 复杂度 |
|------|------|------|--------|
| **Stub** | 提供固定返回值 | 只模拟"结果" | 低 |
| **Mock** | 验证交互过程 | 检查结果 + 调用次数 + 参数 + 顺序 | 高 |
#### GoMock — 接口 Mock
```go
// 1. 定义接口
type DataStore interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
// 2. 用 mockgen 生成 Mock
// mockgen -source=datastore.go -destination=mock_datastore.go
// 3. 在测试中使用
func TestProcessor(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDS := NewMockDataStore(ctrl)
// 设置期望行为
mockDS.EXPECT().Get("user_1").Return([]byte(`{"name":"Alice"}`), nil).Times(1)
mockDS.EXPECT().Set("cache_1", gomock.Any()).Return(nil).AnyTimes()
proc := NewProcessor(mockDS)
result := proc.Process("user_1")
// 验证期望是否满足
// (ctrl 会自动检查)
}
```
#### sqlmock — 数据库 Mock
```go
func TestUserQuery(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
// 模拟查询结果
rows := sqlmock.NewRows([]string{"id", "username"}).
AddRow(1, "alice").
AddRow(2, "bob")
mock.ExpectQuery("SELECT.*FROM users").WillReturnRows(rows)
// 执行查询
results, _ := db.Query("SELECT id, username FROM users")
// ... 验证结果
// 验证所有期望都已满足
mock.ExpectationsWereMet()
}
```
#### httptest — HTTP 服务器 Mock
```go
func TestHandler(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
req := httptest.NewRequest("GET", "/api/test", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
if !bytes.Contains(body, []byte(`"status"`)) {
t.Errorf("body missing 'status' field: %s", body)
}
}
```
### 基准测试(Benchmark)
```go
func BenchmarkFactorial(b *testing.B) {
for i := 0; i < b.N; i++ {
Factorial(10)
}
}
```
运行:
```bash
$ go test -bench=. -benchmem
BenchmarkFactorial-12 1000000000 0.29 ns/op 0 B/op 0 allocs/op
```
> [!tip] 💡 基准测试进阶技巧
> - `-benchmem`:显示内存分配信息
> - `-benchtime=3s`:指定测试时长
> - `b.ResetTimer()`:跳过初始化耗时,只计时核心逻辑
> - `b.RunParallel(...)`:并行基准测试
### 最佳实践 Checklist
> [!tip] 💡 测试编写指南
> 1. **每个公共函数都应该有对应的测试**
> 2. **测试名描述场景而非函数名**:`TestLogin_EmptyPasswordReturnsError` 而非 `TestLogin`
> 3. **每个测试独立**:不依赖其他测试的执行顺序
> 4. **使用表格驱动**:覆盖正常、边界、异常三类场景
> 5. **Mock 外部依赖**:DB、HTTP、文件操作必须替换
> 6. **测试失败信息要有可读性**:用 `t.Errorf("when X, expected Y, got Z")` 格式
> 7. **定期运行 `go test -race ./...`**:检测并发 bug
## 关联笔记
- [[hzh/GolangStar/Go语言进阶/Goroutine]] — 并发测试需注意 race condition
- [[hzh/GolangStar/Go语言框架/gin]] — Gin 路由测试可用 httptest