Skip to content

测试最佳实践

说明

本文在 测试规范 基础上,介绍更高级的测试技巧,包括 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 集成)等方面。核心原则:隔离依赖、覆盖关键路径、自动化运行。

小蚂蚁云团队 · 提供技术支持