Django unit tests with pytest

Django’s built-in test runner works, but pytest makes testing more powerful. Better error messages, parametrize for table-driven tests, and a larger plugin ecosystem for coverage, factories, and database handling.

TL;DR: Set up pytest-django to run your Django test suite, add fixtures, use factories for test data, and get coverage reports.
Stack: Python, Django, pytest, pytest-django, factory-boy
Level: Intermediate
Reading time: ~9 min

Setup Django project with DRF

pip install django djangorestframework
django-admin startproject projtest .
python3 manage.py startapp app

Create the Hero model

class Hero(models.Model):
    name = models.CharField(max_length=100)
    canFly = models.BooleanField()
    genre = models.CharField(max_length=10)

Create use case, protocols, and controller

Organize the project in layers: application/use_cases/, infra/, main/, and presentation/. The controller delegates to the use case, and the use case stays free of HTTP concerns.

class ListHeroesController(APIView):
    def post(self, request):
        try:
            inbound = ListHeroesRequest()
            inbound.canFly = request.data.get("canFly", "false").lower() == "true"
            inbound.genre = request.data.get("genre", "male")
            use_case = ListHeroes()
            result = use_case.execute(inbound)
            outbound = [hero.__dict__ for hero in result]
            return Response({"data": outbound}, status=status.HTTP_200_OK)
        except Exception as e:
            return Response({"message_error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Install pytest

pip install pytest pytest-django

Create pytest.ini

[pytest]
DJANGO_SETTINGS_MODULE = projtest.settings
python_files = tests.py test_*.py *_tests.py

Write tests

# tests/unit/test_list_heroes.py
import pytest
from application.use_cases.list_heroes.list_heroes import ListHeroes
from application.use_cases.list_heroes.protocols.list_heroes_request import ListHeroesRequest
from app.models import Hero

@pytest.mark.django_db
def test_list_heroes_use_case():
    Hero.objects.create(name="Superman", canFly=True, genre="male")
    Hero.objects.create(name="Wonder Woman", canFly=False, genre="female")
    Hero.objects.create(name="Batman", canFly=False, genre="male")

    inbound = ListHeroesRequest()
    inbound.canFly = True
    inbound.genre = "male"

    use_case = ListHeroes()
    result = use_case.execute(inbound)

    assert len(result) == 1
    assert result[0].name == "Superman"
    assert result[0].canFly is True
pytest

Add coverage

pip install pytest-cov
pytest --cov=application --cov-report=term-missing

Create .coveragerc to exclude protocol files from coverage (they are data containers, not logic):

[run]
omit =
    application/use_cases/*/protocols/*

What you’ve built

pytest configured for your Django project with working tests, database fixtures, and coverage reporting.

Next steps

  • Use @pytest.mark.django_db on tests that need database access. Without it, database calls raise an error.
  • Use factory-boy for test data: UserFactory() is cleaner than repeating model setup in every test.
  • Run pytest –reuse-db with pytest-django to skip migrations on repeated test runs, which speeds up the test suite significantly.

Questions or feedback? Find me on LinkedIn or GitHub.

Leave a Comment