Go 单元测试完整指南¶
一份全面的 Go 测试指南,从基础到进阶,涵盖测试最佳实践和 SonarQube 集成。
目录¶
- 基础概念
- 第一个测试
- 测试文件规范
- 测试覆盖率
- 表驱动测试
- Mock 和接口测试
- 测试组织结构
- Makefile 测试目标
- SonarQube 覆盖率集成
- 最佳实践
- 常用工具和库
- 快速参考
- 完整示例
- 总结
1. 基础概念¶
什么是单元测试¶
单元测试是对软件中最小可测试单元(通常是函数或方法)进行验证的过程。在 Go 中,单元测试是内置功能,无需额外框架。
为什么要写测试¶
- 提高代码质量:及早发现 bug
- 重构保障:确保重构不会破坏现有功能
- 文档作用:测试代码展示了函数的预期行为
- 设计驱动:写测试能帮助你思考 API 设计
Go 测试的基本规则¶
| 规则 | 说明 |
|---|---|
| 文件命名 | 测试文件必须以 _test.go 结尾 |
| 函数命名 | 测试函数必须以 Test 开头 |
| 函数签名 | func TestXxx(t *testing.T) |
| 包位置 | 测试文件与被测试文件放在同一包中 |
| 导入包 | 需要导入 testing 包 |
测试类型¶
// 单元测试 - 测试单个函数或方法
func TestAdd(t *testing.T) { }
// 基准测试 - 测试性能
func BenchmarkAdd(b *testing.B) { }
// 示例测试 - 既是文档又是测试
func ExampleAdd() { }
// 子测试 - 分组测试
func TestAdd(t *testing.T) {
t.Run("positive", func(t *testing.T) { })
t.Run("negative", func(t *testing.T) { })
}
// 表格驱动测试 - 多场景测试
func TestAdd(t *testing.T) {
tests := []struct { /* ... */ }
for _, tt := range tests { }
}
2. 第一个测试¶
步骤 1: 创建被测试函数¶
// utils/string.go
package utils
// ToUpper 将字符串转换为大写
func ToUpper(s string) string {
if s == "" {
return ""
}
result := make([]rune, len(s))
for i, r := range s {
if r >= 'a' && r <= 'z' {
result[i] = r - 32
} else {
result[i] = r
}
}
return string(result)
}
步骤 2: 创建测试文件¶
// utils/string_test.go
package utils
import "testing"
func TestToUpper(t *testing.T) {
// Arrange (准备测试数据)
input := "hello"
expected := "HELLO"
// Act (执行被测试函数)
result := ToUpper(input)
// Assert (验证结果)
if result != expected {
t.Errorf("ToUpper(%q) = %q; want %q", input, result, expected)
}
}
步骤 3: 运行测试¶
# 运行当前包的测试
go test
# 运行所有包的测试
go test ./...
# 运行特定包的测试
go test ./utils
# 运行特定测试函数
go test ./utils -run TestToUpper
# 详细输出(显示每个测试)
go test -v ./utils
# 显示覆盖率
go test -cover ./utils
测试输出说明¶
$ go test -v
=== RUN TestToUpper
--- PASS: TestToUpper (0.00s)
PASS
ok utils 0.001s
3. 测试文件规范¶
3.1 文件命名¶
| 源文件 | 测试文件 | 说明 |
|---|---|---|
user.go |
user_test.go |
基本测试文件 |
auth_service.go |
auth_service_test.go |
下划线命名 |
utils/time.go |
time_test.go |
与源文件同目录 |
3.2 包声明¶
// 方式 1: 黑盒测试(推荐)
// 测试代码作为包的外部用户,只能访问导出的标识符
package utils_test
import "yourproject/utils"
func TestPublicFunction(t *testing.T) {
utils.SomeFunction() // 只能测试公开函数
}
// 方式 2: 白盒测试
// 可访问包内部未导出的标识符
package utils
func TestInternalFunction(t *testing.T) {
internalFunction() // 可以测试内部函数
}
选择建议: - 黑盒测试:测试公开 API,更接近实际使用 - 白盒测试:测试内部逻辑,需要注意内部实现变化
3.3 测试函数命名¶
// 好的命名 - 清晰描述测试意图
func TestUserService_CreateUser_Success(t *testing.T) { }
func TestUserService_CreateUser_DuplicateEmail(t *testing.T) { }
func TestUserService_CreateUser_InvalidInput(t *testing.T) { }
// 不好的命名 - 模糊不清
func TestUserService1(t *testing.T) { }
func TestCreate(t *testing.T) { }
func TestFeature(t *testing.T) { }
3.4 测试文件结构¶
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// 测试组
func TestToUpper(t *testing.T) {
// 子测试
t.Run("lowercase input", func(t *testing.T) { })
t.Run("mixed case input", func(t *testing.T) { })
t.Run("empty string", func(t *testing.T) { })
}
// 辅助函数
func setup(t *testing.T) *Config {
t.Helper()
return &Config{}
}
func teardown(t *testing.T, c *Config) {
t.Helper()
// 清理资源
}
4. 测试覆盖率¶
4.1 查看覆盖率¶
# 生成覆盖率报告
go test -coverprofile=coverage.out ./...
# 查看覆盖率百分比(每个函数)
go tool cover -func=coverage.out
# 生成 HTML 覆盖率报告
go tool cover -html=coverage.out -o coverage.html
# 在终端显示覆盖率百分比
go test -cover ./...
# 按包查看覆盖率
go test -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | grep "^total:"
4.2 覆盖率报告解读¶
$ go tool cover -func=coverage.out
github.com/user/project/utils/string.go:10: ToUpper 100.0%
github.com/user/project/utils/string.go:20: ToLower 50.0%
total: 75.0%
4.3 设置覆盖率目标¶
# 检查覆盖率是否达到 80%
go test -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | \
tail -1 | \
awk '{if ($3+0 < 80) {print "Coverage below 80%"; exit 1}}'
4.4 覆盖率模式¶
Go 支持三种覆盖率模式:
| 模式 | 说明 | 推荐场景 |
|---|---|---|
set |
默认,只记录是否覆盖 | 本地开发 |
count |
记录执行次数 | 性能分析 |
atomic |
原子操作,并发安全 | CI/CD 环境 |
# CI/CD 推荐:使用 atomic 模式
go test -coverprofile=coverage.out -covermode=atomic ./...
# 开发环境:使用默认 set 模式
go test -coverprofile=coverage.out ./...
4.5 排除不需要测试的代码¶
// 使用构建标签排除整个文件
// +build !integration
package main
func testIntegration() { }
# 或者在测试时排除特定目录
go test -coverprofile=coverage.out $(go list ./... | grep -v /vendor/)
5. 表驱动测试¶
表驱动测试是 Go 中最推荐的测试模式,可以轻松测试多个场景。
5.1 基本示例¶
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
want time.Duration
wantErr bool
}{
{
name: "valid seconds",
input: "5s",
want: 5 * time.Second,
wantErr: false,
},
{
name: "valid minutes",
input: "10m",
want: 10 * time.Minute,
wantErr: false,
},
{
name: "valid days",
input: "2d",
want: 48 * time.Hour,
wantErr: false,
},
{
name: "invalid input",
input: "invalid",
want: 0,
wantErr: true,
},
{
name: "empty string",
input: "",
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDuration(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDuration(%q) error = %v, wantErr %v",
tt.input, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParseDuration(%q) = %v, want %v",
tt.input, got, tt.want)
}
})
}
}
5.2 使用辅助函数¶
func TestToUpper(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"lowercase", "hello", "HELLO"},
{"mixed case", "HeLLo", "HELLO"},
{"empty string", "", ""},
{"with numbers", "hello123", "HELLO123"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToUpper(tt.input)
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}
5.3 并行测试¶
func TestParallel(t *testing.T) {
tests := []struct {
name string
input string
}{
{"test1", "input1"},
{"test2", "input2"},
{"test3", "input3"},
}
for _, tt := range tests {
tt := tt // 创建局部变量副本
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 标记为可并行运行
// 测试逻辑
})
}
}
5.4 表驱动测试最佳实践¶
// ✅ 好的做法
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed numbers", -2, 3, 1},
{"zeros", 0, 0, 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)
}
})
}
}
// ❌ 不好的做法 - 没有使用子测试
func TestAdd_Bad(t *testing.T) {
if Add(2, 3) != 5 {
t.Error("test 1 failed")
}
if Add(-2, -3) != -5 {
t.Error("test 2 failed")
}
// 如果第一个测试失败,后续测试不会执行
}
6. Mock 和接口测试¶
6.1 使用接口进行 Mock¶
// 定义接口
type UserRepository interface {
FindByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}
// 被测试的服务
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return "", err
}
return user.Name, nil
}
6.2 手动 Mock¶
// mock_user_repository.go
package mocks
type MockUserRepository struct {
users map[int]*User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int]*User),
}
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (m *MockUserRepository) Create(user *User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) Update(user *User) error {
if _, ok := m.users[user.ID]; !ok {
return fmt.Errorf("user not found")
}
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) Delete(id int) error {
delete(m.users, id)
return nil
}
6.3 使用 Mock 进行测试¶
func TestUserService_GetUserName(t *testing.T) {
// Setup
mockRepo := NewMockUserRepository()
mockRepo.Create(&User{ID: 1, Name: "Alice"})
service := &UserService{repo: mockRepo}
// Act
name, err := service.GetUserName(1)
// Assert
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if name != "Alice" {
t.Errorf("got name %q, want %q", name, "Alice")
}
}
6.4 使用 gomock(推荐)¶
# 安装 gomock
go install github.com/golang/mock/mockgen@latest
# 生成 mock
//go:generate go run github.com/golang/mock/mockgen -source=user.go -destination=mock_user_repository.go
func TestUserService_GetUserName_WithMock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
mockRepo.EXPECT().
FindByID(1).
Return(&User{ID: 1, Name: "Alice"}, nil).
Times(1)
service := &UserService{repo: mockRepo}
name, err := service.GetUserName(1)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if name != "Alice" {
t.Errorf("got name %q, want %q", name, "Alice")
}
}
6.5 使用 httptest 测试 HTTP 处理器¶
func TestHandler(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
req := httptest.NewRequest("GET", "/test", nil)
rec := httptest.NewRecorder()
handler(rec, req)
res := rec.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("got status %d, want %d", res.StatusCode, http.StatusOK)
}
data, _ := io.ReadAll(res.Body)
if string(data) != "OK" {
t.Errorf("got body %q, want %q", string(data), "OK")
}
}
6.6 测试 HTTP 客户端¶
func TestAPIClient_CallAPI(t *testing.T) {
// 创建测试服务器
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/users" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"name":"Alice"}`))
}))
defer server.Close()
// 创建客户端并使用测试服务器 URL
client := NewAPIClient(server.URL)
user, err := client.GetUser("1")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got name %q, want %q", user.Name, "Alice")
}
}
7. 测试组织结构¶
7.1 推荐的项目结构¶
project/
├── cmd/
│ └── server/
│ ├── main.go
│ └── main_test.go
├── internal/
│ ├── user/
│ │ ├── user.go # 源代码
│ │ ├── user_test.go # 单元测试
│ │ └── user_integration_test.go # 集成测试
│ └── auth/
│ ├── auth.go
│ └── auth_test.go
├── pkg/
│ └── utils/
│ ├── string.go
│ └── string_test.go
├── test/
│ ├── mocks/ # Mock 文件
│ │ ├── mock_user_repository.go
│ │ └── mock_auth_service.go
│ ├── fixtures/ # 测试数据
│ │ ├── users.json
│ │ └── config.yaml
│ └── testutil/ # 测试工具函数
│ ├── db.go
│ └── http.go
├── go.mod
├── go.sum
├── Makefile
└── README.md
7.2 测试文件组织¶
// user.go - 源代码
package user
type User struct {
ID int
Name string
}
type Repository interface {
FindByID(id int) (*User, error)
Create(user *User) error
}
// user_test.go - 单元测试
package user
import "testing"
func TestUser_Validate(t *testing.T) {
// 单元测试
}
// user_integration_test.go - 集成测试
// +build integration
package user
import "testing"
func TestUserRepository_Integration(t *testing.T) {
// 集成测试(需要数据库等外部依赖)
}
7.3 测试辅助函数¶
// test/testutil/db.go
package testutil
import (
"database/sql"
"testing"
_ "github.com/lib/pq"
)
// SetupTestDB 创建测试数据库
func SetupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("postgres", "test-dsn")
if err != nil {
t.Fatal(err)
}
return db
}
// CleanupTestDB 清理测试数据库
func CleanupTestDB(t *testing.T, db *sql.DB) {
t.Helper()
// 清理逻辑
db.Close()
}
// test/testutil/http.go
package testutil
import "net/http/httptest"
// SetupTestServer 创建测试服务器
func SetupTestServer(handler http.Handler) *httptest.Server {
return httptest.NewServer(handler)
}
7.4 使用测试辅助函数¶
package user
import (
"testing"
"yourproject/test/testutil"
)
func TestUserRepository(t *testing.T) {
db := testutil.SetupTestDB(t)
defer testutil.CleanupTestDB(t, db)
repo := NewRepository(db)
// 测试逻辑
}
8. Makefile 测试目标¶
8.1 基础测试目标¶
# Makefile
.PHONY: test test-cover test-race test-bench test-integration test-all
# 变量
GO := go
GOFLAGS := -v
COVERAGE_FILE := coverage.out
# 运行所有测试
test:
$(GO) test $(GOFLAGS) ./...
# 运行测试并生成覆盖率
test-cover:
$(GO) test $(GOFLAGS) -race -coverprofile=$(COVERAGE_FILE) -covermode=atomic ./...
$(GO) tool cover -html=$(COVERAGE_FILE) -o coverage.html
# 竞态检测
test-race:
$(GO) test $(GOFLAGS) -race ./...
# 基准测试
test-bench:
$(GO) test $(GOFLAGS) -bench=. -benchmem ./...
# 快速测试(跳过慢速测试)
test-short:
$(GO) test $(GOFLAGS) -short ./...
# 运行特定包的测试
test-unit:
$(GO) test $(GOFLAGS) ./internal/...
# 集成测试(需要标签)
test-integration:
$(GO) test $(GOFLAGS) -tags=integration ./...
# 运行所有测试类型
test-all: test test-race test-bench
# 检查覆盖率是否达标
test-coverage-check:
$(GO) test -coverprofile=$(COVERAGE_FILE) ./...
@$(GO) tool cover -func=$(COVERAGE_FILE) | grep total | \
awk '{if ($$3+0 < 80) {print "Coverage below 80%:" $$3; exit 1}}'
8.2 使用 Makefile¶
# 运行所有测试
make test
# 运行测试并生成覆盖率报告
make test-cover
# 检查覆盖率是否达标
make test-coverage-check
# 运行竞态检测
make test-race
# 运行基准测试
make test-bench
9. SonarQube 覆盖率集成¶
9.1 快速开始¶
# 1. 生成覆盖率报告
go test -coverprofile=coverage.out -covermode=atomic ./...
# 2. 运行 SonarQube 扫描
sonar-scanner \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.sourceEncoding=UTF-8
9.2 完整配置示例¶
命令行参数¶
# 基础配置
sonar-scanner \
-Dsonar.projectKey=gotest \
-Dsonar.host.url=https://sonarqube.example.com \
-Dsonar.login=your-token \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.sourceEncoding=UTF-8
# 多个覆盖率文件(合并)
sonar-scanner \
-Dsonar.go.coverage.reportPaths=coverage1.out,coverage2.out
# 排除不需要分析的文件
sonar-scanner \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.exclusions=**/*_test.go,**/vendor/**,**/mock/**
# 指定源码位置
sonar-scanner \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.sources=.
sonar-project.properties¶
# 项目配置
sonar.projectKey=gotest
sonar.projectName=Go Test Project
sonar.projectVersion=1.0
# 源码配置
sonar.sources=.
sonar.sourceEncoding=UTF-8
# Go 覆盖率配置(注意参数名顺序)
sonar.go.coverage.reportPaths=coverage.out
# 排除文件
sonar.exclusions=**/*_test.go,**/vendor/**,**/mock/**
# 测试报告(如果有)
sonar.go.tests.reportPaths=test-report.json
9.3 CI/CD 集成示例¶
GitHub Actions¶
name: SonarQube Scan
on:
push:
branches: [ main, develop ]
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarqube:
name: SonarQube Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 重要:完整历史用于 blame
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run tests with coverage
run: |
go test -coverprofile=coverage.out -covermode=atomic ./...
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.go.coverage.reportPaths=coverage.out
-Dsonar.sourceEncoding=UTF-8
GitLab CI¶
sonarqube:
stage: test
image: golang:1.21
script:
- go test -coverprofile=coverage.out -covermode=atomic ./...
- sonar-scanner
-Dsonar.projectKey=${CI_PROJECT_NAME}
-Dsonar.go.coverage.reportPaths=coverage.out
-Dsonar.sourceEncoding=UTF-8
only:
- merge_requests
- main
- develop
Jenkins Pipeline¶
pipeline {
agent any
tools {
go '1.21'
}
environment {
SONAR_TOKEN = credentials('sonarqube-token')
SONAR_HOST_URL = 'https://sonarqube.example.com'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Test') {
steps {
sh 'go test -coverprofile=coverage.out -covermode=atomic ./...'
}
}
stage('SonarQube Scan') {
steps {
script {
scannerHome = tool 'SonarQubeScanner'
withSonarQubeEnv('SonarQube') {
sh """
${scannerHome}/bin/sonar-scanner \
-Dsonar.projectKey=${JOB_NAME} \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.sourceEncoding=UTF-8
"""
}
}
}
}
}
post {
always {
// 归并覆盖率报告
archiveArtifacts artifacts: 'coverage.out', fingerprint: true
}
success {
echo 'SonarQube analysis completed successfully!'
}
failure {
echo 'Pipeline failed. Check logs for details.'
}
}
}
使用说明:
-
配置 Jenkins 凭据:
// 在 Jenkins 中添加 SonarQube Token // Manage Jenkins → Managed credentials → Add credentials // 类型: Secret text // ID: sonarqube-token -
配置 SonarQube Scanner 工具:
// Manage Jenkins → Global Tool Configuration // SonarQube Scanner → Add SonarQube Scanner // Name: SonarQubeScanner -
配置 SonarQube Server:
// Manage Jenkins → Configure System → SonarQube servers // Name: SonarQube // Server URL: https://sonarqube.example.com // Server authentication token: (使用上面的凭据)
声明式 Pipeline(推荐):
pipeline {
agent any
parameters {
string(name: 'SONAR_PROJECT_KEY', defaultValue: 'gotest', description: 'SonarQube project key')
}
stages {
stage('Build and Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'go test -v ./...'
}
}
stage('Coverage') {
steps {
sh 'go test -coverprofile=coverage.out -covermode=atomic ./...'
}
}
}
}
stage('Quality Gate') {
steps {
script {
scannerHome = tool 'SonarQubeScanner'
withSonarQubeEnv('SonarQube') {
sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectKey=${params.SONAR_PROJECT_KEY}"
}
}
// 等待 Quality Gate 结果
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
}
9.4 验证配置¶
# 1. 确认覆盖率文件生成
go test -coverprofile=coverage.out ./...
ls -lh coverage.out
# 2. 查看覆盖率内容
go tool cover -func=coverage.out | tail -5
# 3. 本地测试 SonarQube 扫描(调试模式)
sonar-scanner -X \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.sourceEncoding=UTF-8
# 4. 检查 SonarQube 日志中的关键信息
# 查找: "Parsing coverage report"
# 查找: "Coverage information was not collected"
9.5 Makefile 集成¶
.PHONY: test-coverage sonar-scan
# 生成覆盖率报告
test-coverage:
go test -coverprofile=coverage.out -covermode=atomic ./...
# SonarQube 扫描
sonar-scan: test-coverage
sonar-scanner \
-Dsonar.go.coverage.reportPaths=coverage.out \
-Dsonar.sourceEncoding=UTF-8
# 完整流程
ci: test-coverage sonar-scan
9.6 故障排查检查清单¶
- 使用正确的参数名:
sonar.go.coverage.reportPaths - 覆盖率文件路径正确且文件存在
- 使用
atomic覆盖模式(CI 环境) - 在项目根目录运行测试和扫描
- SonarQube Token 有正确权限
- 源码编码设置为 UTF-8
- 排除不必要的文件(测试、vendor)
- Git 历史完整(CI 环境)
10. 最佳实践¶
10.1 测试命名¶
// ✅ 好的命名
func TestUserService_CreateUser_Success(t *testing.T) {}
func TestUserService_CreateUser_DuplicateEmail(t *testing.T) {}
func TestUserService_CreateUser_InvalidInput(t *testing.T) {}
// ❌ 不好的命名
func TestUserService1(t *testing.T) {}
func TestCreate(t *testing.T) {}
10.2 使用子测试¶
func TestParseDuration(t *testing.T) {
t.Run("valid input", func(t *testing.T) {
t.Run("seconds", func(t *testing.T) {})
t.Run("minutes", func(t *testing.T) {})
t.Run("hours", func(t *testing.T) {})
})
t.Run("invalid input", func(t *testing.T) {})
}
10.3 测试隔离¶
func TestWithSetupAndTeardown(t *testing.T) {
// Setup
db := setupTestDB(t)
defer cleanupTestDB(t, db)
// Test
// ...
}
func setupTestDB(t *testing.T) *sql.DB {
t.Helper() // 标记为辅助函数
// 创建测试数据库
}
func cleanupTestDB(t *testing.T, db *sql.DB) {
t.Helper()
// 清理资源
}
10.4 测试超时¶
func TestWithTimeout(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 测试逻辑
}
10.5 使用基准测试¶
func BenchmarkToUpper(b *testing.B) {
input := "hello world"
for i := 0; i < b.N; i++ {
ToUpper(input)
}
}
// 运行基准测试
// go test -bench=. -benchmem
10.6 测试模糊化(Fuzzing)- Go 1.18+¶
func FuzzParseDuration(f *testing.F) {
// 添加种子语料库
f.Add("5s")
f.Add("10m")
f.Fuzz(func(t *testing.T, input string) {
d, err := ParseDuration(input)
if err != nil {
return // 无效输入是允许的
}
if d < 0 {
t.Errorf("duration should not be negative: %v", d)
}
})
}
// 运行模糊化测试
// go test -fuzz=FuzzParseDuration
10.7 环境变量控制¶
func TestIntegration(t *testing.T) {
if os.Getenv("INTEGRATION_TEST") != "true" {
t.Skip("set INTEGRATION_TEST=true to run integration tests")
}
// 集成测试逻辑
}
10.8 使用 testify 断言¶
import "github.com/stretchr/testify/assert"
func TestWithTestify(t *testing.T) {
assert.Equal(t, expected, actual)
assert.NoError(t, err)
assert.Nil(t, obj)
assert.True(t, condition)
assert.Contains(t, slice, element)
assert.Len(t, slice, 3)
}
11. 常用工具和库¶
11.1 testify - 断言和 Mock¶
go get github.com/stretchr/testify
import "github.com/stretchr/testify/assert"
func TestWithTestify(t *testing.T) {
assert.Equal(t, expected, actual)
assert.NoError(t, err)
assert.Nil(t, obj)
assert.True(t, condition)
assert.Contains(t, slice, element)
assert.Len(t, slice, 3)
}
11.2 gomock - Mock 生成¶
go install github.com/golang/mock/mockgen@latest
# 生成 mock
mockgen -source=user.go -destination=mock_user.go
11.3 go-sqlmock - SQL Mock¶
go get github.com/DATA-DOG/go-sqlmock
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice")
mock.ExpectQuery("SELECT \\* FROM users").
WillReturnRows(rows)
// 使用 db 进行测试
11.4 httptest - HTTP 测试¶
// 标准库内置,无需安装
import "net/http/httptest"
12. 快速参考¶
12.1 常用测试命令¶
# 基础测试
go test # 当前包
go test ./... # 所有包
go test -v ./... # 详细输出
go test -run TestFoo ./... # 运行特定测试
# 覆盖率
go test -cover ./...
go test -coverprofile=c.out ./...
go tool cover -html=c.out
# 性能测试
go test -race ./... # 竞态检测
go test -bench=. ./... # 基准测试
go test -cpuprofile=cpu.out ./... # CPU 性能分析
# 其他
go test -short ./... # 跳过耗时测试
go test -count=1 ./... # 禁用缓存
go test -timeout 30s ./... # 设置超时
12.2 测试标志¶
| 标志 | 说明 |
|---|---|
-v |
详细输出 |
-run |
运行匹配的测试 |
-cover |
显示覆盖率 |
-race |
竞态检测 |
-short |
跳过耗时测试 |
-parallel |
并行测试数 |
-count |
运行次数 |
12.3 测试函数签名¶
| 类型 | 签名 | 用途 |
|---|---|---|
| 单元测试 | func TestXxx(t *testing.T) |
测试功能 |
| 基准测试 | func BenchmarkXxx(b *testing.B) |
测试性能 |
| 示例测试 | func ExampleXxx() |
文档示例 |
| 模糊测试 | func FuzzXxx(f *testing.F) |
随机输入测试 |
13. 完整示例¶
13.1 完整的测试文件¶
// utils/string_test.go
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestToUpper(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "lowercase letters",
input: "hello",
expected: "HELLO",
},
{
name: "mixed case",
input: "HeLLo",
expected: "HELLO",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "with numbers",
input: "hello123",
expected: "HELLO123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToUpper(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestToUpper_WithSpecialChars(t *testing.T) {
// 使用 require 遇到错误立即停止
require.NotNil(t, ToUpper)
result := ToUpper("hello@world!")
assert.Equal(t, "HELLO@WORLD!", result)
}
14. 总结¶
14.1 单元测试 Checklist¶
- 测试文件以
_test.go结尾 - 测试函数以
Test开头 - 使用表驱动测试覆盖多个场景
- 测试命名清晰描述测试意图
- 使用
t.Helper()标记辅助函数 - 测试之间相互独立
- 使用 Mock 隔离外部依赖
- 保持测试代码简洁
- 目标覆盖率 ≥ 80%
- 定期运行
go test -race
14.2 学习资源¶
附录¶
A. 常见错误¶
// ❌ 错误:没有检查错误
result, _ := SomeFunction()
// ✅ 正确:检查错误
result, err := SomeFunction()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
B. 性能测试技巧¶
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 并行执行的测试代码
}
})
}
C. 测试覆盖率命令¶
# 查看特定包的覆盖率
go test -coverprofile=coverage.out ./path/to/package
go tool cover -func=coverage.out
# 生成 HTML 报告
go tool cover -html=coverage.out -o coverage.html
open coverage.html
最后更新: 2026年5月14日