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

8.5 KiB
Raw Blame History

tags, create time
tags create time
go
golang
单元测试
testing
Mock
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() — 带输出注释的示例

基础测试

被测试代码

package gotest

func Factorial(n int) int {
    if n <= 0 {
        return 1
    }
    return n * Factorial(n-1)
}

测试代码

func TestFactorial(t *testing.T) {
    result := Factorial(5)
    if result != 120 {
        t.Errorf("Factorial(5) = %d; want 120", result)
    }
}

运行:

$ go test -v
=== RUN   TestFactorial
--- PASS: TestFactorial (0.00s)
PASS

表格驱动测试(Table-Driven Tests)

[!tip] 💡 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)
            }
        })
    }
}

运行结果:

=== RUN   TestFactorial
=== RUN   TestFactorial/zero
=== RUN   TestFactorial/one
=== RUN   TestFactorial/five
=== RUN   TestFactorial/ten
--- PASS: TestFactorial (0.00s)

[!tip] 💡 可复用的表格驱动模板

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 风格测试

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

// 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

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

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)

func BenchmarkFactorial(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Factorial(10)
    }
}

运行:

$ 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

关联笔记