本文共 6067 字,大约阅读时间需要 20 分钟。
# settings_test.py (继承主配置)from .settings import *# 使用内存数据库加速DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' }}# 禁用密码哈希加速PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]# 关闭调试模式DEBUG = FalseTEMPLATE_DEBUG = False
pip install pytest pytest-django pytest-mock coverage
myapp/├── tests/│ ├── __init__.py│ ├── test_models.py│ ├── test_views.py│ ├── test_forms.py│ └── test_utils/│ └── test_validators.py├── models.py└── views.py
from django.test import TestCasefrom .models import Productfrom .factories import ProductFactoryclass ProductModelTest(TestCase): @classmethod def setUpTestData(cls): cls.product = ProductFactory(name="Test Product") def test_name_label(self): field_label = self.product._meta.get_field('name').verbose_name self.assertEqual(field_label, 'product name') def test_price_max_digits(self): max_digits = self.product._meta.get_field('price').max_digits self.assertEqual(max_digits, 10) def test_get_absolute_url(self): url = self.product.get_absolute_url() self.assertEqual(url, f'/products/{self.product.id}/')
from django.urls import reversefrom rest_framework.test import APITestCaseclass ProductViewTest(APITestCase): def setUp(self): self.product = ProductFactory() self.list_url = reverse('product-list') self.detail_url = reverse('product-detail', args=[self.product.id]) def test_list_view_status_code(self): response = self.client.get(self.list_url) self.assertEqual(response.status_code, 200) def test_detail_view_content(self): response = self.client.get(self.detail_url) self.assertContains(response, self.product.name) def test_unauthorized_create(self): data = { 'name': 'New', 'price': 99.9 } response = self.client.post(self.list_url, data) self.assertEqual(response.status_code, 401)
from django.test import TestCasefrom .forms import ProductFormclass ProductFormTest(TestCase): def test_valid_data(self): form = ProductForm(data={ 'name': 'Valid Product', 'price': 50.00, 'category': 1 }) self.assertTrue(form.is_valid()) def test_invalid_price(self): form = ProductForm(data={ 'name': 'Test', 'price': -10 }) self.assertFalse(form.is_valid()) self.assertIn('price', form.errors)
from rest_framework import statusfrom .factories import UserFactoryclass ProductAPITest(APITestCase): def setUp(self): self.user = UserFactory() self.client.force_login(self.user) def test_create_product(self): data = { "name": "API Product", "price": 199.9 } response = self.client.post('/api/products/', data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data['name'], "API Product")
from django.test import RequestFactoryfrom .middleware import CustomMiddlewareclass MiddlewareTest(TestCase): def test_custom_header(self): factory = RequestFactory() request = factory.get('/') middleware = CustomMiddleware(lambda req: None) middleware(request) self.assertIn('X-Custom-Header', request.META)
# factories.pyimport factoryfrom .models import Product, Userclass UserFactory(factory.django.DjangoModelFactory): class Meta: model = User username = factory.Sequence(lambda n: f'user_{n}') email = factory.LazyAttribute(lambda u: f'{u.username}@example.com') is_active = Trueclass ProductFactory(factory.django.DjangoModelFactory): class Meta: model = Product name = factory.Faker('word') price = factory.Faker('pydecimal', left_digits=3, right_digits=2, positive=True) owner = factory.SubFactory(UserFactory)
# products.json[ { "model": "shop.Product", "pk": 1, "fields": { "name": "Fixture Product", "price": "99.99" } }]class TestWithFixtures(TestCase): fixtures = ['products.json'] def test_fixture_data(self): product = Product.objects.get(pk=1) self.assertEqual(product.price, Decimal('99.99'))
from unittest.mock import patchfrom .services import PaymentServiceclass PaymentTest(TestCase): @patch('myapp.services.requests.post') def test_payment_success(self, mock_post): mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "success" } result = PaymentService.process(amount=100) self.assertTrue(result.is_success)
from freezegun import freeze_timeclass TimeSensitiveTest(TestCase): @freeze_time("2023-01-01") def test_new_year(self): from datetime import date self.assertEqual(date.today(), date(2023, 1, 1))
# .coveragerc[run]source = myapp/omit = */migrations/* */tests/* __init__.py[report]show_missing = truefail_under = 85
coverage run -m pytestcoverage html
pytest -n auto
pytest --lfpytest --ff
@pytest.mark.slowdef test_slow_operation(): time.sleep(5)pytest -m "not slow"
测试金字塔策略强调测试的价值与开发工作的平衡。通过优先测试高风险或高复杂度的功能,可以最大限度地减少遗留问题。
原则 | 说明 |
---|---|
FAST原则 | 测试应快速、孤立、可重复、自验证、及时的 |
Red-Green-Refactor | 测试驱动开发流程 |
最小化数据库交互 | 使用mock和内存数据库来加速测试 |
单断言原则 | 每个测试应验证一个行为 |
最小化测试数据 | 使用工厂创建测试数据 |
注重接口行为 | 关注外部用户看到的接口行为,而非内部实现 |
特性 | django.test | pytest |
---|---|---|
用例组织 | Class-based | 函数式+Class |
参数化测试 | 需第三方库 | 内置支持 |
Fixtures 管理 | Fixtures | 内置+插件 |
Mock 支持 | unittest.mock | 内置monkeypatch |
插件生态 | 有限 | 丰富 |
执行速度 | 较慢 | 快(并行) |
pytest + pytest-django + factory_boy + coverage
完整示例项目:Django Testing Example
通过本指南,您将能够为Django应用构建完备的测试防护网,覆盖从模型到API的全栈功能,大幅提升代码质量和交付信心!
转载地址:http://pprfk.baihongyu.com/