Files
server/docs/TESTING.md
T
2026-01-16 15:49:34 +08:00

399 lines
8.7 KiB
Markdown

# 单元测试指南
本项目已添加前后端单元测试支持,以提升代码质量和可维护性。
## 后端测试 (Go - Goravel)
基于 [Goravel 测试框架](https://www.goravel.dev/zh_CN/testing/getting-started.html),使用 `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
```
### 运行测试
```bash
# 运行所有测试
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 命令创建测试:
```bash
go run . artisan make:test unit/MyServiceTest
go run . artisan make:test feature/MyFeatureTest
```
### 测试示例
```go
// 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/
└── ...
```
**优点:**
- 测试代码与源码分离
- 与后端测试结构一致
### 运行测试
```bash
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 |
### 测试示例
```javascript
// 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
// Go: TestSuiteName + TestMethodName
func (s *TokenServiceTestSuite) TestHashToken_ValidInput()
// JavaScript: describe + it
describe('validators', () => {
describe('required', () => {
it('应该拒绝空字符串', () => {})
})
})
```
### 2. 表格驱动测试 (Go)
```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)
```javascript
import { vi } from 'vitest'
// Mock 模块
vi.mock('element-plus', () => ({
ElMessage: {
success: vi.fn(),
error: vi.fn()
}
}))
// 验证调用
expect(ElMessage.success).toHaveBeenCalledWith('操作成功')
```
### 4. 异步测试
```javascript
// async/await
it('应该处理异步操作', async () => {
const result = await asyncFunction()
expect(result).toBe('expected')
})
// Fake timers
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.restoreAllMocks())
```
---
## CI/CD 集成
### GitHub Actions 示例
```yaml
# .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` | 权限列表、角色权限绑定 |
### 运行集成测试
```bash
# 运行所有集成测试(需要 Docker)
go test -v ./tests/feature/...
# 运行特定测试
go test -v -run TestAdminApiTestSuite ./tests/feature/...
go test -v -run TestBlacklistApiTestSuite ./tests/feature/...
```
### 测试示例
```go
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 框架的推荐做法,参考 [官方文档](https://www.goravel.dev/zh_CN/testing/getting-started.html)。
### Q: 如何添加新测试?
**A:**
- 后端:`go run . artisan make:test unit/MyTest`
- 前端:在对应模块的 `__tests__` 目录创建 `.test.js` 文件