# 测试最佳实践 :::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 集成)等方面。核心原则:隔离依赖、覆盖关键路径、自动化运行。