Go 的内置测试框架、代码规范与工程化实践


一、单元测试

1.1 测试文件命名

// 测试文件以 _test.go 结尾
math.go          → math_test.go
user.go          → user_test.go
utils/string.go  → utils/string_test.go

1.2 基本测试函数

// math.go
package math

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

// math_test.go
package math

import "testing"

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

// 运行测试
// go test ./...
// go test -v ./...(详细输出)

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

Go 社区最推崇的测试模式:

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 1, 2, 3},
        {"negative numbers", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -1, 1, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// 运行特定子测试
// go test -run TestAdd/positive_numbers

1.4 测试辅助函数

// 断言辅助函数(Go 标准库没有内置 assert,但可以自己写)
func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper()  // 标记为辅助函数,错误信息显示调用者行号
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func TestAdd(t *testing.T) {
    assertEqual(t, Add(1, 2), 3)
}

t.Helper():标记辅助函数,错误时显示调用方的行号而非辅助函数内部行号。


二、测试进阶

2.1 TestMain — 测试入口

func TestMain(m *testing.M) {
    // 测试前的全局设置
    setup()

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

    // 测试后的清理
    teardown()

    os.Exit(code)
}

2.2 基准测试(Benchmark)

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

// 运行
// go test -bench=. -benchmem
// 输出:BenchmarkAdd-8  1000000000  0.28 ns/op  0 B/op  0 allocs/op

// 带缓存的基准测试
func BenchmarkAddCached(b *testing.B) {
    a, result := 1, 2  // 预设值
    b.ResetTimer()     // 重置计时器
    for i := 0; i < b.N; i++ {
        Add(a, result)
    }
}

2.3 示例测试(Example)

// Example 既是测试,也是文档(会出现在 godoc 中)
func ExampleAdd() {
    result := Add(1, 2)
    fmt.Println(result)
    // Output: 3
}

func ExampleAdd_negative() {
    result := Add(-1, -2)
    fmt.Println(result)
    // Output: -3
}

// 运行
// go test -run Example

2.4 测试覆盖率

// 查看覆盖率
// go test -cover ./...

// 生成覆盖率报告
// go test -coverprofile=coverage.out ./...
// go tool cover -html=coverage.out -o coverage.html

2.5 并行测试

func TestAdd(t *testing.T) {
    tests := []struct{ a, b, want int }{
        {1, 2, 3}, {4, 5, 9},
    }

    for _, tt := range tests {
        t.Run("...", func(t *testing.T) {
            t.Parallel()  // 标记为可并行运行
            result := Add(tt.a, tt.b)
            if result != tt.want {
                t.Errorf("got %d, want %d", result, tt.want)
            }
        })
    }
}

三、Mock 与依赖注入

3.1 接口驱动的 Mock

// 定义接口(消费方)
type UserStore interface {
    FindByID(id int) (*User, error)
}

// 实现(生产方)
type MySQLStore struct{ db *sql.DB }

func (s *MySQLStore) FindByID(id int) (*User, error) { ... }

// 测试用 Mock
type MockStore struct {
    Users map[int]*User
}

func (m *MockStore) FindByID(id int) (*User, error) {
    user, ok := m.Users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return user, nil
}

// Service 依赖接口
type UserService struct {
    store UserStore
}

func (s *UserService) GetUser(id int) (*User, error) {
    return s.store.FindByID(id)
}

// 测试
func TestGetUser(t *testing.T) {
    mock := &MockStore{
        Users: map[int]*User{1: {ID: 1, Name: "Alice"}},
    }
    svc := &UserService{store: mock}

    user, err := svc.GetUser(1)
    if err != nil {
        t.Fatal(err)
    }
    if user.Name != "Alice" {
        t.Errorf("got %s, want Alice", user.Name)
    }
}

3.2 第三方 Mock 库

// testify/mock — 常用的 mock 框架
import "github.com/stretchr/testify/mock"

type MockStore struct {
    mock.Mock
}

func (m *MockStore) FindByID(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

func TestGetUser(t *testing.T) {
    m := new(MockStore)
    m.On("FindByID", 1).Return(&User{Name: "Alice"}, nil)

    svc := &UserService{store: m}
    user, _ := svc.GetUser(1)

    m.AssertExpectations(t)
}

四、Go 代码规范

4.1 命名规范

// 包名:小写,简短,不下划线
package httpclient   // ✅
package http_client  // ❌

// 导出标识符:大写驼峰
func GetUserData() {}  // ✅
func Get_user_data() {}  // ❌

// 非导出标识符:小写驼峰
func validateInput() {}  // ✅
func ValidateInput() {}  // 只在需要导出时用大写

// 接口命名:方法少的用 -er 后缀
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }

4.2 格式化与工具

// go fmt — 统一代码格式(不需要讨论缩进/花括号位置)
// go vet — 静态分析,发现常见错误
// golangci-lint — 集成多种 lint 工具

// 安装 golangci-lint
// go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

// 运行
// golangci-lint run ./...

4.3 go generate

// 在代码中嵌入生成指令
//go:generate stringer -type=MyType

// 运行所有生成指令
// go generate ./...

4.4 代码组织原则

// 每个文件不要太大,通常 200-400 行
// 如果一个文件超过 500 行,考虑拆分

// 包的设计原则:
// 1. 包应该小而专注(单一职责)
// 2. 避免循环依赖
// 3. 接口在消费方定义
// 4. 导出的 API 要精心设计(一旦导出,就要保持兼容)

五、项目构建与发布

5.1 编译优化

// 基本编译
go build -o myapp

// 去除调试信息(减小二进制大小)
go build -ldflags="-s -w" -o myapp

// 注入版本信息
go build -ldflags="-X main.Version=1.0.0 -X main.BuildTime=2024-01-01" -o myapp

5.2 交叉编译

// 交叉编译不需要 CGO 的项目非常简单
// Linux -> Windows
GOOS=windows GOARCH=amd64 go build -o myapp.exe

// 所有平台组合
GOOS=linux   GOARCH=amd64 go build -o myapp-linux-amd64
GOOS=linux   GOARCH=arm64 go build -o myapp-linux-arm64
GOOS=darwin  GOARCH=arm64 go build -o myapp-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe

5.3 Makefile 示例

.PHONY: build test lint clean

build:
	go build -ldflags="-s -w" -o bin/myapp

test:
	go test -v -race ./...

lint:
	golangci-lint run ./...

clean:
	rm -rf bin/

# 交叉编译所有平台
build-all:
	GOOS=linux GOARCH=amd64 go build -o bin/myapp-linux-amd64
	GOOS=darwin GOARCH=arm64 go build -o bin/myapp-darwin-arm64
	GOOS=windows GOARCH=amd64 go build -o bin/myapp-windows-amd64.exe

六、常用工具速查

工具命令作用
go testgo test -v -race ./...运行测试
go test -covergo test -cover ./...查看覆盖率
go test -benchgo test -bench=. -benchmem运行基准测试
go vetgo vet ./...静态分析
go fmtgo fmt ./...代码格式化
gofmtgofmt -d .查看格式差异
goimportsgoimports -d .自动管理 import
golangci-lintgolangci-lint run综合 lint
go tool pprofgo tool pprof性能分析
go generatego generate ./...运行代码生成
go docgo doc fmt.Println查看文档
go listgo list -m all列出模块依赖

七、学习阶段一总结

至此,Go 基础语法的 10 篇笔记全部完成:

篇目核心内容
01-环境与工具链安装配置、Go Modules、核心命令
02-变量与类型变量声明、基本类型、类型转换
03-复合类型切片、Map、结构体、指针
04-流程控制if/for/switch/defer
05-错误处理error 接口、panic/recover、错误链
06-函数与方法多返回值、闭包、方法接收者
07-接口与多态隐式接口、空接口、面向接口编程
08-包与模块包组织、可见性、项目结构
09-并发编程基础Goroutine、Channel、Select、sync
10-测试与工程实践单元测试、基准测试、代码规范

上一篇:09-并发编程基础 下一阶段:03-并发编程 — Channel 高级模式、Context、并发安全数据结构