pytest-django
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesepytest-django
pytest-django
Deep Knowledge: Usewith technology:mcp__documentation__fetch_docsfor comprehensive documentation on all fixtures, markers, and advanced patterns.pytest-django
深度知识:使用并指定技术为mcp__documentation__fetch_docs,可获取所有fixture、标记和高级模式的完整文档。pytest-django
Setup
设置
toml
undefinedtoml
undefinedpyproject.toml
pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings.test"
python_files = ["tests.py", "test_.py", "_tests.py"]
undefined[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings.test"
python_files = ["tests.py", "test_.py", "_tests.py"]
undefined@pytest.mark.django_db Options
@pytest.mark.django_db 选项
python
@pytest.mark.django_db # basic DB access, rollback
@pytest.mark.django_db(transaction=True) # real commits (TransactionTestCase)
@pytest.mark.django_db(transaction=True, reset_sequences=True) # reset auto-increment
@pytest.mark.django_db(databases=["default", "analytics"]) # multiple DBs
@pytest.mark.django_db(databases="__all__") # all databases
@pytest.mark.django_db(serialized_rollback=True) # for data migration testspython
@pytest.mark.django_db # 基础数据库访问,自动回滚
@pytest.mark.django_db(transaction=True) # 真实提交(TransactionTestCase)
@pytest.mark.django_db(transaction=True, reset_sequences=True) # 重置自增序列
@pytest.mark.django_db(databases=["default", "analytics"]) # 多数据库
@pytest.mark.django_db(databases="__all__") # 所有数据库
@pytest.mark.django_db(serialized_rollback=True) # 用于数据迁移测试Core Fixtures
核心Fixture
python
def test_basic(db): # DB access fixture (use in other fixtures)
pass
def test_client(client): # django.test.Client
response = client.get("/")
assert response.status_code == 200
def test_admin(admin_client): # pre-logged-in superuser client
response = admin_client.get("/admin/")
assert response.status_code == 200
def test_rf(rf, admin_user): # RequestFactory (bypasses middleware)
request = rf.get("/items/")
request.user = admin_user
response = my_view(request)
def test_settings(settings): # modify settings, auto-reverted
settings.DEBUG = True
settings.CACHES = {"default": {"BACKEND": "...DummyCache"}}
def test_mail(mailoutbox): # access sent emails
send_mail("Subject", "Body", "from@x.com", ["to@x.com"])
assert len(mailoutbox) == 1
assert mailoutbox[0].subject == "Subject"
def test_user(django_user_model): # AUTH_USER_MODEL
user = django_user_model.objects.create_user("alice", password="pass")
assert user.check_password("pass")python
def test_basic(db): # 数据库访问fixture(可在其他fixture中使用)
pass
def test_client(client): # django.test.Client
response = client.get("/")
assert response.status_code == 200
def test_admin(admin_client): # 已登录的超级用户客户端
response = admin_client.get("/admin/")
assert response.status_code == 200
def test_rf(rf, admin_user): # RequestFactory(绕过中间件)
request = rf.get("/items/")
request.user = admin_user
response = my_view(request)
def test_settings(settings): # 修改配置,自动还原
settings.DEBUG = True
settings.CACHES = {"default": {"BACKEND": "...DummyCache"}}
def test_mail(mailoutbox): # 查看已发送邮件
send_mail("Subject", "Body", "from@x.com", ["to@x.com"])
assert len(mailoutbox) == 1
assert mailoutbox[0].subject == "Subject"
def test_user(django_user_model): # AUTH_USER_MODEL
user = django_user_model.objects.create_user("alice", password="pass")
assert user.check_password("pass")Authentication Patterns
认证模式
python
def test_force_login(client, django_user_model):
user = django_user_model.objects.create_user("alice", password="pass")
client.force_login(user) # fastest, no password check
response = client.get("/private/")
assert response.status_code == 200
def test_client_login(client, django_user_model):
django_user_model.objects.create_user("alice", password="pass")
client.login(username="alice", password="pass")
response = client.get("/private/")
assert response.status_code == 200python
def test_force_login(client, django_user_model):
user = django_user_model.objects.create_user("alice", password="pass")
client.force_login(user) # 最快方式,无需密码校验
response = client.get("/private/")
assert response.status_code == 200
def test_client_login(client, django_user_model):
django_user_model.objects.create_user("alice", password="pass")
client.login(username="alice", password="pass")
response = client.get("/private/")
assert response.status_code == 200DRF APIClient
DRF APIClient
python
import pytest
from rest_framework.test import APIClient
from rest_framework import status
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def auth_client(api_client, django_user_model, db):
user = django_user_model.objects.create_user("alice", password="pass")
api_client.force_authenticate(user=user)
return api_client
@pytest.mark.django_db
def test_create(auth_client):
resp = auth_client.post("/api/items/", {"name": "Widget"}, format="json")
assert resp.status_code == status.HTTP_201_CREATED
assert resp.data["name"] == "Widget"
@pytest.mark.django_db
def test_token_auth(api_client, django_user_model):
from rest_framework.authtoken.models import Token
user = django_user_model.objects.create_user("alice", password="pass")
token = Token.objects.create(user=user)
api_client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
resp = api_client.get("/api/me/")
assert resp.status_code == status.HTTP_200_OKpython
import pytest
from rest_framework.test import APIClient
from rest_framework import status
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def auth_client(api_client, django_user_model, db):
user = django_user_model.objects.create_user("alice", password="pass")
api_client.force_authenticate(user=user)
return api_client
@pytest.mark.django_db
def test_create(auth_client):
resp = auth_client.post("/api/items/", {"name": "Widget"}, format="json")
assert resp.status_code == status.HTTP_201_CREATED
assert resp.data["name"] == "Widget"
@pytest.mark.django_db
def test_token_auth(api_client, django_user_model):
from rest_framework.authtoken.models import Token
user = django_user_model.objects.create_user("alice", password="pass")
token = Token.objects.create(user=user)
api_client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
resp = api_client.get("/api/me/")
assert resp.status_code == status.HTTP_200_OKfactory_boy Integration
factory_boy集成
python
undefinedpython
undefinedfactories.py
factories.py
from factory.django import DjangoModelFactory
import factory
class UserFactory(DjangoModelFactory):
class Meta:
model = "auth.User"
django_get_or_create = ("username",)
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")
password = factory.django.Password("testpass123")class ArticleFactory(DjangoModelFactory):
class Meta:
model = "myapp.Article"
title = factory.Sequence(lambda n: f"Article {n}")
author = factory.SubFactory(UserFactory)
body = factory.Faker("paragraph")from factory.django import DjangoModelFactory
import factory
class UserFactory(DjangoModelFactory):
class Meta:
model = "auth.User"
django_get_or_create = ("username",)
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")
password = factory.django.Password("testpass123")class ArticleFactory(DjangoModelFactory):
class Meta:
model = "myapp.Article"
title = factory.Sequence(lambda n: f"Article {n}")
author = factory.SubFactory(UserFactory)
body = factory.Faker("paragraph")conftest.py
conftest.py
@pytest.fixture
def user(db):
return UserFactory()
@pytest.fixture
def article(db):
return ArticleFactory()
undefined@pytest.fixture
def user(db):
return UserFactory()
@pytest.fixture
def article(db):
return ArticleFactory()
undefinedTesting Signals
测试信号
python
from unittest.mock import MagicMock
from django.db.models.signals import post_save
@pytest.mark.django_db
def test_signal_fires(db):
handler = MagicMock()
post_save.connect(handler, sender=Article)
try:
Article.objects.create(title="Test", body="Content")
assert handler.called
assert handler.call_args[1]["created"] is True
finally:
post_save.disconnect(handler, sender=Article)python
from unittest.mock import MagicMock
from django.db.models.signals import post_save
@pytest.mark.django_db
def test_signal_fires(db):
handler = MagicMock()
post_save.connect(handler, sender=Article)
try:
Article.objects.create(title="Test", body="Content")
assert handler.called
assert handler.call_args[1]["created"] is True
finally:
post_save.disconnect(handler, sender=Article)Testing Management Commands
测试管理命令
python
from io import StringIO
from django.core.management import call_command
@pytest.mark.django_db
def test_command_output():
out = StringIO()
call_command("my_command", stdout=out, verbosity=2)
assert "Success" in out.getvalue()python
from io import StringIO
from django.core.management import call_command
@pytest.mark.django_db
def test_command_output():
out = StringIO()
call_command("my_command", stdout=out, verbosity=2)
assert "Success" in out.getvalue()Async Views (Django 4.1+)
异步视图(Django 4.1+)
python
@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_async_view(async_client):
response = await async_client.get("/async-endpoint/")
assert response.status_code == 200python
@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_async_view(async_client):
response = await async_client.get("/async-endpoint/")
assert response.status_code == 200Query Count Assertions
查询次数断言
python
def test_no_n_plus_one(client, django_assert_max_num_queries):
with django_assert_max_num_queries(5):
response = client.get("/api/articles/")
assert response.status_code == 200
def test_exact_queries(django_assert_num_queries):
with django_assert_num_queries(1):
Article.objects.get(pk=1)python
def test_no_n_plus_one(client, django_assert_max_num_queries):
with django_assert_max_num_queries(5):
response = client.get("/api/articles/")
assert response.status_code == 200
def test_exact_queries(django_assert_num_queries):
with django_assert_num_queries(1):
Article.objects.get(pk=1)on_commit Callbacks
on_commit回调
python
def test_email_on_commit(client, mailoutbox, django_capture_on_commit_callbacks):
with django_capture_on_commit_callbacks(execute=True):
client.post("/orders/", {"item_id": 1})
assert len(mailoutbox) == 1python
def test_email_on_commit(client, mailoutbox, django_capture_on_commit_callbacks):
with django_capture_on_commit_callbacks(execute=True):
client.post("/orders/", {"item_id": 1})
assert len(mailoutbox) == 1Recommended conftest.py
推荐的conftest.py
python
undefinedpython
undefinedconftest.py
conftest.py
import pytest
@pytest.fixture(autouse=True)
def fast_password_hasher(settings):
settings.PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
@pytest.fixture(autouse=True)
def dummy_cache(settings):
settings.CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
@pytest.fixture(autouse=True)
def email_backend(settings):
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
undefinedimport pytest
@pytest.fixture(autouse=True)
def fast_password_hasher(settings):
settings.PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
@pytest.fixture(autouse=True)
def dummy_cache(settings):
settings.CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
@pytest.fixture(autouse=True)
def email_backend(settings):
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
undefinedAnti-Patterns
反模式
| Anti-Pattern | Solution |
|---|---|
| Use default (rollback) unless you need real commits |
| Use |
| Not resetting factory sequences | |
Not using | Prefer |
Official docs: https://pytest-django.readthedocs.io/
| 反模式 | 解决方案 |
|---|---|
所有测试都用 | 除非需要真实提交,否则使用默认的回滚模式 |
会话级fixture中使用 | 对会话范围使用 |
| 不重置工厂序列 | 在fixture中调用 |
不使用 | 优先使用 |