Skip to content

单元测试规范

核心原则

单元测试必须覆盖核心业务逻辑,使用标准测试工具,确保测试独立可重复。

允许的做法

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

相关文档