单元测试规范
核心原则
单元测试必须覆盖核心业务逻辑,使用标准测试工具,确保测试独立可重复。
允许的做法
Go 后端测试
测试文件命名
service.go → service_test.go
handler.go → handler_test.go
utils.go → utils_test.go基础测试
go
// ✅ 正确 - 测试函数命名
package service
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService_Create(t *testing.T) {
// Setup
db := setupTestDB(t)
defer teardownTestDB(t, db)
service := NewService(db, &Config{})
// Test
req := &CreateRequest{
Name: "Test Name",
Value: "Test Value",
}
result, err := service.Create(context.Background(), req)
// Assert
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "Test Name", result.Name)
}表格驱动测试
go
// ✅ 正确 - 表格驱动测试
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "user@example.com", true},
{"missing @", "userexample.com", false},
{"missing domain", "user@", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
assert.Equal(t, tt.want, got)
})
}
}Mock 使用
go
// ✅ 正确 - 使用 testify/mock
import "github.com/stretchr/testify/mock"
type MockService struct {
mock.Mock
}
func (m *MockService) Create(ctx context.Context, req *CreateRequest) (*Result, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*Result), args.Error(1)
}
// 测试中使用 Mock
func TestHandler_Create(t *testing.T) {
mockService := new(MockService)
handler := NewHandler(mockService)
// 设置 Mock 期望
mockService.On("Create", mock.Anything, mock.Anything).
Return(&Result{ID: 1, Name: "Test"}, nil)
// 测试 Handler
// ...
// 验证 Mock 调用
mockService.AssertExpectations(t)
}测试数据库
go
// ✅ 正确 - 使用内存数据库
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestDB(t *testing.T) *gorm.DB {
// 使用 SQLite 内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// 自动迁移表结构
err = db.AutoMigrate(&Model{})
require.NoError(t, err)
return db
}
func teardownTestDB(t *testing.T, db *gorm.DB) {
sqlDB, _ := db.DB()
sqlDB.Close()
}错误处理测试
go
// ✅ 正确 - 测试错误场景
func TestService_Create_ValidationError(t *testing.T) {
db := setupTestDB(t)
defer teardownTestDB(t, db)
service := NewService(db, &Config{})
// 测试参数为空
req := &CreateRequest{Name: ""}
result, err := service.Create(context.Background(), req)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "参数不完整")
}
func TestService_GetByID_NotFound(t *testing.T) {
db := setupTestDB(t)
defer teardownTestDB(t, db)
service := NewService(db, &Config{})
result, err := service.GetByID(context.Background(), 999)
assert.Error(t, err)
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
assert.Nil(t, result)
}React 前端测试
组件测试
typescript
// ✅ 正确 - 组件测试
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './index';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent title="Test Title" />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
it('handles click event', async () => {
const handleClick = jest.fn();
render(<MyComponent onClick={handleClick} />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});异步测试
typescript
// ✅ 正确 - 异步数据加载测试
import { render, screen, waitFor } from '@testing-library/react';
import DataList from './index';
describe('DataList', () => {
it('loads and displays data', async () => {
render(<DataList />);
// 等待加载完成
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
expect(screen.getByText('Item 2')).toBeInTheDocument();
});
});Mock API 请求
typescript
// ✅ 正确 - Mock API
import { render, screen, waitFor } from '@testing-library/react';
import * as api from '@/services/api';
import DataList from './index';
jest.mock('@/services/api');
describe('DataList', () => {
it('fetches and displays data', async () => {
const mockData = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];
(api.getList as jest.Mock).mockResolvedValue({
success: true,
data: mockData,
});
render(<DataList />);
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
});
});测试覆盖率
覆盖率要求
bash
# Go 后端
# Service 层: > 80%
# Handler 层: > 60%
# Utils 层: > 90%
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# React 前端
# 组件: > 70%
# Hooks: > 80%
# Utils: > 90%
npm test -- --coverage覆盖率配置
json
// ✅ 正确 - package.json
{
"jest": {
"coverageThreshold": {
"global": {
"branches": 70,
"functions": 70,
"lines": 70,
"statements": 70
}
}
}
}禁止的做法
- 禁止 测试依赖外部服务
- 禁止 测试之间相互依赖
- 避免 过度 Mock
go
// ❌ 错误 - 依赖外部数据库
func TestService_Create(t *testing.T) {
db, _ := gorm.Open(mysql.Open("user:pass@tcp(localhost:3306)/db"))
// 测试依赖真实数据库
}
// ✅ 正确 - 使用内存数据库
func TestService_Create(t *testing.T) {
db, _ := gorm.Open(sqlite.Open(":memory:"))
// 测试使用内存数据库
}
// ❌ 错误 - 测试之间依赖
func TestA(t *testing.T) {
globalVar = "test" // 修改全局状态
}
func TestB(t *testing.T) {
assert.Equal(t, "test", globalVar) // 依赖 TestA
}
// ✅ 正确 - 测试独立
func TestA(t *testing.T) {
localVar := "test"
assert.Equal(t, "test", localVar)
}
func TestB(t *testing.T) {
localVar := "test"
assert.Equal(t, "test", localVar)
}测试命令
bash
# Go 后端
# 运行所有测试
go test ./...
# 运行指定包测试
go test ./services
# 运行指定测试函数
go test -run TestService_Create
# 生成覆盖率报告
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# React 前端
# 运行所有测试
npm test
# 运行指定测试文件
npm test -- ComponentName.test.tsx
# 监听模式
npm test -- --watch
# 生成覆盖率报告
npm test -- --coverage