init
This commit is contained in:
+398
@@ -0,0 +1,398 @@
|
||||
# 单元测试指南
|
||||
|
||||
本项目已添加前后端单元测试支持,以提升代码质量和可维护性。
|
||||
|
||||
## 后端测试 (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` 文件
|
||||
Reference in New Issue
Block a user