Files
2026-09-12 23:12:15 +08:00

351 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试最佳实践
:::tip 说明
本文在 [测试规范](/zh/volume7/testing.html) 基础上,介绍更高级的测试技巧,包括 Mock、Fixture、API 测试、覆盖率实战等。
:::
## Mock 与 Patch
### 什么是 Mock
Mock 用于模拟外部依赖,隔离被测代码:
```python
# 不使用 Mock:测试依赖 Redis 连接
def test_get_captcha():
# 如果 Redis 未启动,测试会失败
captcha = generate_captcha() # 依赖 Redis 存储
# 使用 Mock:隔离 Redis 依赖
@patch('utils.redis.get_redis_connection')
def test_get_captcha(mock_redis):
mock_redis.return_value = MagicMock()
captcha = generate_captcha() # 不依赖真实 Redis
```
### unittest.mock.patch
```python
from unittest.mock import patch, MagicMock
class UserServiceTest(TestCase):
"""用户服务测试"""
@patch('application.user.services.get_redis_connection')
def test_login_with_mock_redis(self, mock_redis):
"""测试登录(Mock Redis)"""
# 配置 Mock
mock_redis.return_value.get.return_value = None
# 执行测试
result = login_service(request)
# 验证结果
self.assertEqual(result['code'], 0)
@patch('utils.common.get_client_ip')
def test_record_log_with_mock_ip(self, mock_ip):
"""测试记录日志(Mock IP 获取)"""
mock_ip.return_value = '127.0.0.1'
record_login_log(request, 'admin', 1, 0)
# 验证日志记录
```
### Mock 返回值配置
```python
# Mock 返回固定值
mock_func.return_value = 'mocked_value'
# Mock 返回不同值(按调用顺序)
mock_func.side_effect = ['first', 'second', 'third']
# Mock 抛出异常
mock_func.side_effect = Exception('connection failed')
# Mock 根据参数返回不同值
def side_effect_func(arg):
if arg == 'valid':
return True
return False
mock_func.side_effect = side_effect_func
```
## Fixture 管理
### Django TestCase setUp
```python
class ExampleServiceTest(TestCase):
"""案例服务测试"""
def setUp(self):
"""测试前准备:创建测试数据"""
self.user = User.objects.create(
username='testuser',
realname='测试用户',
password=make_password('123456'),
status=1
)
self.example = Example.objects.create(
name='测试案例',
type=1,
status=1,
sort=1,
create_user='testuser'
)
def tearDown(self):
"""测试后清理(可选,Django TestCase 自动回滚)"""
pass
```
### 批量创建测试数据
```python
def create_test_examples(count=10):
"""批量创建测试数据"""
examples = []
for i in range(count):
examples.append(Example(
name=f'案例{i}',
type=i % 3 + 1,
status=1,
sort=i,
create_user='admin'
))
Example.objects.bulk_create(examples)
return examples
```
### Factory Boy(推荐大型项目)
```python
# pip install factory-boy
import factory
from application.example.models import Example
class ExampleFactory(factory.django.DjangoModelFactory):
"""案例工厂"""
class Meta:
model = Example
name = factory.Sequence(lambda n: f'案例{n}')
type = 1
status = 1
sort = factory.Sequence(lambda n: n)
create_user = 'admin'
# 使用
example = ExampleFactory() # 创建单个
examples = ExampleFactory.create_batch(10) # 批量创建
example = ExampleFactory(name='自定义名称') # 自定义字段
```
## API 测试
### Django Client
```python
from django.test import TestCase, Client
class ExampleViewTest(TestCase):
"""案例视图测试"""
def setUp(self):
self.client = Client()
# 创建测试数据
Example.objects.create(name='案例1', type=1, status=1, sort=1)
def test_page_without_login(self):
"""测试未登录访问"""
response = self.client.get('/example/page')
self.assertIn(response.status_code, [401, 302])
def test_page_with_login(self):
"""测试登录后访问"""
# 模拟登录
self.client.force_login(User.objects.first())
response = self.client.get('/example/page')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['code'], 0)
self.assertIn('records', data['data'])
```
### RequestFactory
```python
from django.test import TestCase, RequestFactory
class ExampleServiceTest(TestCase):
"""案例服务测试"""
def setUp(self):
self.factory = RequestFactory()
def test_get_page(self):
"""测试分页查询"""
request = self.factory.get('/example/page', {'pageNo': 1, 'pageSize': 10})
result = get_example_page(request)
self.assertEqual(result.status_code, 200)
def test_add(self):
"""测试添加"""
request = self.factory.post(
'/example/add',
data=json.dumps({'name': '新案例', 'type': 1}),
content_type='application/json'
)
result = add_example(request)
self.assertEqual(result.status_code, 200)
```
## 常见测试场景
### 权限测试
```python
class PermissionTest(TestCase):
"""权限测试"""
def test_admin_bypass_permission(self):
"""测试管理员跳过权限检查"""
admin = User.objects.get(id=1)
self.client.force_login(admin)
response = self.client.get('/example/page')
self.assertEqual(response.status_code, 200)
def test_user_permission_check(self):
"""测试普通用户权限检查"""
user = User.objects.create(username='normal_user')
self.client.force_login(user)
response = self.client.get('/example/page')
# 无权限时应返回 401
self.assertIn(response.status_code, [401, 403])
```
### 文件上传测试
```python
from django.core.files.uploadedfile import SimpleUploadedFile
class UploadTest(TestCase):
"""文件上传测试"""
def test_upload_image(self):
"""测试上传图片"""
image = SimpleUploadedFile(
name='test.png',
content=b'\x89PNG\r\n...',
content_type='image/png'
)
response = self.client.post('/upload/uploadFile', {'file': image})
self.assertEqual(response.status_code, 200)
```
### 软删除测试
```python
class SoftDeleteTest(TestCase):
"""软删除测试"""
def test_soft_delete(self):
"""测试软删除"""
example = Example.objects.create(name='待删除', type=1, status=1)
# 软删除
example.is_delete = True
example.save()
# 验证查询不到
self.assertFalse(
Example.objects.filter(is_delete=False, id=example.id).exists()
)
# 验证包含已删除可以查到
self.assertTrue(
Example.objects.filter(id=example.id).exists()
)
```
## 测试覆盖率
### 配置 coverage
```bash
# 安装
pip install coverage
# 运行测试并生成覆盖率
coverage run --source='application' manage.py test
# 查看报告
coverage report
# 生成 HTML 报告
coverage html
# 查看未覆盖的行
coverage report --show-missing
```
### 覆盖率目标
| 层级 | 目标 | 说明 |
|------|------|------|
| Model 层 | 90%+ | 模型方法、字段约束 |
| Service 层 | 80%+ | 业务逻辑、异常处理 |
| View 层 | 70%+ | 请求处理、权限校验 |
| 工具函数 | 90%+ | 通用工具函数 |
### CI 集成
```yaml
# .github/workflows/test.yml
- name: Run tests
run: |
coverage run --source='application' manage.py test
coverage report --fail-under=80
```
## 测试组织
### 按模块组织
```
application/
├── user/
│ ├── models.py
│ ├── services.py
│ ├── views.py
│ └── tests.py # 该模块的测试
├── role/
│ └── tests.py
└── example/
└── tests.py
```
### 按类型组织
```
tests/
├── test_models.py # 模型测试
├── test_services.py # Service 层测试
├── test_views.py # View 层测试
├── test_forms.py # Form 层测试
└── test_utils.py # 工具函数测试
```
## 总结
测试最佳实践涵盖 Mock/patch(隔离外部依赖)、Fixture(测试数据管理)、API 测试(Client/RequestFactory)、常见场景(权限/上传/软删除)、覆盖率(coverage + CI 集成)等方面。核心原则:隔离依赖、覆盖关键路径、自动化运行。