Files
2026-01-16 15:49:34 +08:00

8.7 KiB

单元测试指南

本项目已添加前后端单元测试支持,以提升代码质量和可维护性。

后端测试 (Go - Goravel)

基于 Goravel 测试框架,使用 testify/suite 组织测试。

目录结构

tests/
├── test_case.go           # 测试基类
├── feature/               # 功能测试(集成测试)
│   ├── main_test.go       # 测试入口
│   ├── example_test.go
│   └── ...
├── unit/                  # 单元测试
│   ├── main_test.go       # 测试入口
│   ├── token_service_test.go
│   ├── tree_service_test.go
│   ├── ip_matcher_test.go
│   └── pagination_test.go
└── services/              # 服务层测试
    └── main_test.go

运行测试

# 运行所有测试
go test ./tests/...

# 运行单元测试
go test ./tests/unit/...

# 运行功能测试
go test ./tests/feature/...

# 显示详细输出
go test -v ./tests/...

# 运行特定测试
go test -v -run TestTokenService ./tests/unit/...

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

创建测试

使用 Artisan 命令创建测试:

go run . artisan make:test unit/MyServiceTest
go run . artisan make:test feature/MyFeatureTest

测试示例

// tests/unit/token_service_test.go
package unit

import (
    "testing"
    "github.com/stretchr/testify/suite"
    "goravel/tests"
)

type TokenServiceTestSuite struct {
    suite.Suite
    tests.TestCase
}

func TestTokenServiceTestSuite(t *testing.T) {
    suite.Run(t, new(TokenServiceTestSuite))
}

func (s *TokenServiceTestSuite) SetupTest() {
    // 每个测试前执行
}

func (s *TokenServiceTestSuite) TearDownTest() {
    // 每个测试后执行
}

func (s *TokenServiceTestSuite) TestHashToken() {
    // 测试 token 哈希
    s.Equal(64, len(hashToken("test")))
}

func (s *TokenServiceTestSuite) TestGenerateRandomToken() {
    token1 := generateRandomToken()
    token2 := generateRandomToken()
    
    s.Len(token1, 40)
    s.NotEqual(token1, token2)
}

前端测试 (Vitest)

目录结构

前端测试支持两种组织方式:

方式一:共置模式(当前使用) 推荐

测试文件放在被测模块旁边的 __tests__ 目录:

html/src/
├── composables/
│   ├── useCrud.js
│   ├── useDebounce.js
│   └── __tests__/              # 测试放在模块旁边
│       ├── useCrud.test.js
│       └── useDebounce.test.js
├── utils/
│   ├── validation.js
│   ├── storage.js
│   └── __tests__/
│       ├── validation.test.js
│       └── storage.test.js
└── components/
    └── __tests__/
        └── ...

优点:

  • 测试文件与源码紧密关联,便于查找和维护
  • 符合 Vue/React 社区最佳实践
  • 模块独立性强,便于移动或删除

方式二:集中模式

所有测试放在根目录的 tests 目录:

html/
├── src/
│   ├── composables/
│   ├── utils/
│   └── components/
└── tests/                      # 所有测试集中存放
    ├── unit/
    │   ├── composables/
    │   │   ├── useCrud.test.js
    │   │   └── useDebounce.test.js
    │   └── utils/
    │       ├── validation.test.js
    │       └── storage.test.js
    └── integration/
        └── ...

优点:

  • 测试代码与源码分离
  • 与后端测试结构一致

运行测试

cd html

# 交互式监听模式
npm test

# 单次运行
npm run test:run

# 生成覆盖率报告
npm run test:coverage

当前测试文件

测试文件 覆盖内容 测试数量
composables/__tests__/useDebounce.test.js 防抖功能 12
composables/__tests__/useCrud.test.js CRUD 操作 19
utils/__tests__/validation.test.js 验证器函数 41
utils/__tests__/storage.test.js Storage 工具 13

测试示例

// src/utils/__tests__/validation.test.js
import { describe, it, expect } from 'vitest'
import { validators } from '../validation'

describe('validators', () => {
  describe('required', () => {
    it('应该拒绝空字符串', () => {
      expect(validators.required('')).not.toBe(true)
    })

    it('应该接受有效字符串', () => {
      expect(validators.required('hello')).toBe(true)
    })
  })

  describe('email', () => {
    it('应该接受有效邮箱', () => {
      expect(validators.email('test@example.com')).toBe(true)
    })
  })
})

测试最佳实践

1. 命名约定

// Go: TestSuiteName + TestMethodName
func (s *TokenServiceTestSuite) TestHashToken_ValidInput()

// JavaScript: describe + it
describe('validators', () => {
  describe('required', () => {
    it('应该拒绝空字符串', () => {})
  })
})

2. 表格驱动测试 (Go)

func (s *TokenServiceTestSuite) TestHashToken() {
    tests := []struct {
        name     string
        input    string
        expected int
    }{
        {"正常 token", "test-token", 64},
        {"空 token", "", 64},
    }
    
    for _, tt := range tests {
        s.Run(tt.name, func() {
            got := hashToken(tt.input)
            s.Len(got, tt.expected)
        })
    }
}

3. Mock 使用 (JavaScript)

import { vi } from 'vitest'

// Mock 模块
vi.mock('element-plus', () => ({
  ElMessage: {
    success: vi.fn(),
    error: vi.fn()
  }
}))

// 验证调用
expect(ElMessage.success).toHaveBeenCalledWith('操作成功')

4. 异步测试

// async/await
it('应该处理异步操作', async () => {
  const result = await asyncFunction()
  expect(result).toBe('expected')
})

// Fake timers
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.restoreAllMocks())

CI/CD 集成

GitHub Actions 示例

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  backend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v4
        with:
          go-version: '1.24'
      - run: go test -v ./tests/...

  frontend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: cd html && npm install
      - run: cd html && npm run test:run

测试覆盖率目标

模块类型 当前状态 目标覆盖率
工具函数 已添加 80%+
Composables 已添加 (TypeScript) 70%+
Services 已添加 60%+
Controllers 已添加(集成测试) 40%+

Controller 集成测试

后端 Controller 集成测试位于 tests/feature/ 目录:

测试文件

文件 覆盖内容
admin_api_test.go 管理员登录、信息、列表、角色、菜单、部门、日志等
blacklist_api_test.go 黑名单 CRUD、IP格式验证、批量删除
permission_test.go 权限列表、角色权限绑定

运行集成测试

# 运行所有集成测试(需要 Docker)
go test -v ./tests/feature/...

# 运行特定测试
go test -v -run TestAdminApiTestSuite ./tests/feature/...
go test -v -run TestBlacklistApiTestSuite ./tests/feature/...

测试示例

func (s *AdminApiTestSuite) TestLogin_Success() {
    body := strings.NewReader(`{"username":"admin","password":"admin123"}`)
    resp, err := s.Http(s.T()).
        WithHeader("Content-Type", "application/json").
        Post("/api/admin/login", body)

    s.Require().NoError(err)
    resp.AssertSuccessful()

    content, err := resp.Content()
    s.Require().NoError(err)

    var result map[string]any
    json.Unmarshal([]byte(content), &result)
    s.Equal(float64(200), result["code"])
}

注意事项

  • 集成测试需要 Docker 环境(自动创建测试数据库和 Redis)
  • 每个测试会执行 RefreshDatabase() 重置数据库
  • 使用 Seed() 填充测试数据

常见问题

Q: 前端测试应该放在哪里?

A: 推荐使用共置模式(__tests__ 目录),这是 Vue/React 社区的最佳实践。

Q: 后端测试为什么放在 tests 目录?

A: 这是 Goravel 框架的推荐做法,参考 官方文档

Q: 如何添加新测试?

A:

  • 后端:go run . artisan make:test unit/MyTest
  • 前端:在对应模块的 __tests__ 目录创建 .test.js 文件