后端测试指南
测试金字塔
╱ ╲
╱ E2E ╲
╱ ───── ╲
╱ 集成测试 ╲
╱ ───────── ╲
╱ 单元测试 ╲
╱ ───────────── ╲
╱ 基础单元 ╲
- 基础单元测试:测试单个函数或方法,速度快,覆盖率高
- 集成测试:测试组件间的交互(数据库、外部服务)
- E2E 测试:模拟用户场景,覆盖完整流程
单元测试(Go 示例)
基本测试
// math.go
func Add(a, b int) int {
return a + b
}
// math_test.go
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
表格驱动测试
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
hasErr bool
}{
{"positive", 10, 2, 5, false},
{"zero", 10, 0, 0, true},
{"negative", -10, 2, -5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.hasErr && err == nil {
t.Error("expected error but got none")
}
if !tt.hasErr && result != tt.expected {
t.Errorf("got %d, want %d", result, tt.expected)
}
})
}
}
使用 Mock
type UserRepository interface {
FindByID(id string) (*User, error)
}
type mockRepo struct {
users map[string]*User
}
func (m *mockRepo) FindByID(id string) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, ErrNotFound
}
func TestUserService_GetUser(t *testing.T) {
mock := &mockRepo{
users: map[string]*User{
"1": {ID: "1", Name: "Alice"},
},
}
svc := NewUserService(mock)
user, err := svc.GetUser("1")
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
集成测试
数据库测试
// 使用 testcontainers 启动 PostgreSQL
func TestUserRepository(t *testing.T) {
ctx := context.Background()
postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:16-alpine",
Env: map[string]string{
"POSTGRES_DB": "test",
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
},
ExposedPorts: []string{"5432/tcp"},
},
Started: true,
})
require.NoError(t, err)
defer postgres.Terminate(ctx)
// 连接数据库并执行测试
db, _ := sql.Open("postgres", connectionString)
repo := NewUserRepository(db)
user, err := repo.Create(ctx, &User{Name: "Bob"})
assert.NoError(t, err)
assert.NotEmpty(t, user.ID)
}
E2E 测试
func TestCreateUserFlow(t *testing.T) {
// 启动 HTTP 服务
server := httptest.NewServer(router())
defer server.Close()
// 发送请求
resp, err := http.Post(
server.URL+"/api/users",
"application/json",
strings.NewReader(`{"name":"Charlie","email":"charlie@test.com"}`),
)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
// 验证结果
var user User
json.NewDecoder(resp.Body).Decode(&user)
assert.NotEmpty(t, user.ID)
assert.Equal(t, "Charlie", user.Name)
}
最佳实践
测试命名
func Test[FunctionName]_[Scenario]_[ExpectedResult](t *testing.T) {
// ...
}
// 示例
func TestDivide_ByZero_ReturnsError(t *testing.T) {}
func TestCreateUser_InvalidEmail_ReturnsBadRequest(t *testing.T) {}
覆盖率目标
- 关键业务逻辑:90%+
- Controller/Handler:70%+
- 集成测试:覆盖主要场景
- 不盲目追求 100%,关注核心逻辑
持续集成中的测试
# GitHub Actions
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: test
steps:
- uses: actions/checkout@v4
- run: go test ./... -race -coverprofile=coverage.out
- run: go tool cover -func=coverage.out