Django Testing mit pytest: Testabdeckung professionell aufbauen
Inhalt
In meinen Projekten ist eine gute Testsuite der Unterschied zwischen mutigem und ängstiglichem Refactoring. Hier zeige ich den kompletten Stack den ich nutze.
Setup: pytest-django konfigurieren
pip install pytest pytest-django pytest-cov factory_boy
# pytest.ini oder pyproject.toml
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings.test
python_files = tests.py test_*.py *_tests.py
addopts = --reuse-db -v # --reuse-db: DB nicht nach jedem Run löschen
# settings/test.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:", # In-Memory für maximale Geschwindigkeit
}
}
CELERY_TASK_ALWAYS_EAGER = True # Celery synchron ausführenFixtures und Factory Boy
# factories.py
import factory
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
from myapp.models import Article, Category
class UserFactory(DjangoModelFactory):
class Meta:
model = get_user_model()
username = factory.Sequence(lambda n: f"user_{n}")
email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")
password = factory.PostGenerationMethodCall("set_password", "password123")
class CategoryFactory(DjangoModelFactory):
class Meta:
model = Category
name = factory.Sequence(lambda n: f"Kategorie {n}")
class ArticleFactory(DjangoModelFactory):
class Meta:
model = Article
title = factory.Faker("sentence", nb_words=6, locale="de_DE")
author = factory.SubFactory(UserFactory)
category = factory.SubFactory(CategoryFactory)
status = "draft"# Fixtures als pytest-Fixtures
import pytest
from .factories import UserFactory, ArticleFactory
@pytest.fixture
def user(db):
return UserFactory()
@pytest.fixture
def article(db, user):
return ArticleFactory(author=user, status="published")Unit Tests für Models und Services
# test_models.py
import pytest
from .factories import ArticleFactory
@pytest.mark.django_db
class TestArticleModel:
def test_str_representation(self):
article = ArticleFactory(title="Mein Artikel")
assert str(article) == "Mein Artikel"
def test_publish_sets_status(self):
article = ArticleFactory(status="draft")
article.publish()
assert article.status == "published"
assert article.published_at is not None
def test_unpublished_articles_excluded_from_qs(self):
ArticleFactory(status="published")
ArticleFactory(status="draft")
assert Article.objects.published().count() == 1API-Tests mit DRF
# test_views.py
import pytest
from rest_framework.test import APIClient
from .factories import UserFactory, ArticleFactory
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def auth_client(api_client, user):
api_client.force_authenticate(user=user)
return api_client
@pytest.mark.django_db
class TestArticleAPI:
def test_list_returns_200(self, api_client):
ArticleFactory.create_batch(3, status="published")
response = api_client.get("/api/articles/")
assert response.status_code == 200
assert len(response.data["results"]) == 3
def test_create_requires_auth(self, api_client):
response = api_client.post("/api/articles/", {"title": "Test"})
assert response.status_code == 401
def test_create_article(self, auth_client):
data = {"title": "Neuer Artikel", "status": "draft"}
response = auth_client.post("/api/articles/", data)
assert response.status_code == 201
assert response.data["title"] == "Neuer Artikel"Mocking und Patching
from unittest.mock import patch, MagicMock
@pytest.mark.django_db
def test_email_sent_on_publish(article):
with patch("myapp.tasks.send_publication_email.delay") as mock_email:
article.publish()
mock_email.assert_called_once_with(article.id)
@pytest.mark.django_db
def test_external_api_call():
with patch("myapp.services.requests.get") as mock_get:
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"data": "test"}
)
result = fetch_external_data()
assert result == {"data": "test"}Coverage und CI-Integration
# Coverage-Report generieren
pytest --cov=myapp --cov-report=html --cov-report=term-missing
# .coveragerc
[run]
omit =
*/migrations/*
*/tests/*
manage.py
*/settings/*
[report]
fail_under = 80 # Test schlaägt fehl unter 80% Coverage