The go-to resource for upgrading Python, Django, Flask, and your dependencies.

Migrating Django Tests from unittest to pytest: 7 Patterns for Using Fixtures Instead of setUp


When a snake sheds its skin, it doesn’t just discard the old; it reveals a new, more efficient layer underneath, allowing for better growth and adaptation. This molting process is essential for reptiles to grow, as their rigid skin cannot stretch. By shedding the old, restrictive layer, they enable efficient movement and thermoregulation.

In Django testing, unittest’s setUp method acts like that rigid old skin. It repeats database creation and mocking for every single test, restricting speed and parallelism. Migrating to pytest fixtures is like molting: we shed this repetitive overhead to reveal a reusable, scoped, and parallel-ready testing structure.

Django unittest setUp → pytest fixtures: Legacy setUp repeats DB creates/mocks per test → 3s/test suite → 1.2s with fixtures. Fixtures: scoped reuse, params, autouse. pytest-django handles LiveServerTestCase, transactions. 7 patterns + pytest.ini setup, full Django 5.1 project migration. Benchmarks: tox/pytest parallel xdist 4x speedup. Targets: “django migrate tests to pytest”, “pytest fixtures instead of setup django”, “unittest setUp pytest fixture”.

Why Migrate Django Tests to pytest?

Unittest setUp runs for every test or class, which means database factories (often 200ms each) run repeatedly, causing suite scaling issues. Pytest fixtures, however, offer lazy evaluation and flexible scopes (function, class, module, session) with yield-based teardown. The pytest-django plugin provides the django_db fixture, which combines the benefits of Django’s setUpTestData with transaction rollback for test isolation.

Aspectunittest setUppytest Fixtures
ReusePer class/testfunc/class/module/session
TeardowntearDownyield/finally
ParamsManual loops@pytest.mark.parametrize
ParallelSequentialxdist —numprocesses=4
Django DBTestCase mixindjango_db(block=False)

pytest.ini:

[pytest]
DJANGO_SETTINGS_MODULE = yourproject.settings.test
python_files = tests.py test_*.py *_tests.py
addopts = -v --ds=yourproject.settings.test --reuse-db
testpaths = tests/

Install: pip install pytest pytest-django pytest-xdist factory-boy

Pattern 1: Basic Model Instance (Function-Scoped Fixture)

Unittest:

from django.test import TestCase
from .models import User

class UserTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(username='test', email='test@example.com')

    def test_user_str(self):
        self.assertEqual(str(self.user), 'test')

Pytest Fixture:

import pytest
from .models import User

@pytest.fixture
def user(db):
    return User.objects.create(username='test', email='test@example.com')

def test_user_str(user):
    assert str(user) == 'test'

Gain: db autouses transaction rollback. Fixture reusable.

Pattern 2: Class-Scoped Shared Data (No Repeats)

Unittest setUpClass:

class UserTest(TestCase):
    @classmethod
    def setUpClass(cls):
        cls.shared_user = User.objects.create(username='shared')

    def test_a(self):
        pass

Pytest class scope:

@pytest.fixture(scope='class')
def shared_user(db):
    return User.objects.create(username='shared')

class TestUser:
    def test_a(self, shared_user):
        assert shared_user.username == 'shared'

Gain: Runs once per class.

Pattern 3: Module-Scoped Factories (factory-boy)

Unittest:

def setUp(self):
    self.user = UserFactory()  # Repeat per test

Pytest:

import factory
from factory.django import DjangoModelFactory

class UserFactory(DjangoModelFactory):
    class Meta:
        model = User
    username = factory.Sequence(lambda n: f'user{n}')

@pytest.fixture(scope='module')
def user_factory(db):
    return UserFactory

def test_users(user_factory):
    u1 = user_factory()
    u2 = user_factory()
    assert u1 != u2

Gain: Module reuse, sequences.

Pattern 4: Mock External API (mocker fixture)

Unittest:

from unittest.mock import patch, Mock

class ApiTest(TestCase):
    def setUp(self):
        self.patcher = patch('myapp.services.api_call')
        self.mock_api = self.patcher.start()

    def tearDown(self):
        self.patcher.stop()

Pytest (pytest-mock):

import pytest

def test_api(mocker):
    mock_api = mocker.patch('myapp.services.api_call')
    mock_api.return_value = {'ok': True}
    result = myapp.services.call_api()
    assert result == {'ok': True}

Gain: mocker autouse per test, no manual stop.

Pattern 5: Database Transaction Rollback (django_db)

Unittest TransactionTestCase (slow, full commit):

from django.test import TransactionTestCase

Pytest:

def test_transactional(db_transaction):  # django_db + transaction=True
    user = User.objects.create(...)
    # Changes committed, visible to other processes

Gain: pytest --reuse-db caches DB.

Pattern 6: Parametrized Fixtures

Unittest loops:

def test_valid_emails(self):
    for email in ['a@b.com', 'valid@test.co']:
        user = User(email=email)
        assert user.is_valid_email()

Pytest:

@pytest.fixture(params=['a@b.com', 'valid@test.co'])
def valid_email(request):
    return request.param

def test_valid_emails(valid_email):
    assert User(email=valid_email).is_valid_email()

Gain: --collect-only shows params.

Pattern 7: Autouse Fixture for Test Client

Unittest:

from django.test import Client
class ViewTest(TestCase):
    def setUp(self):
        self.client = Client()

@pytest.fixture(autouse=True)
def client():
    from django.test import Client
    return Client()

def test_view(client):
    resp = client.get('/users/')
    assert resp.status_code == 200

Gain: Auto-injects client everywhere.

Benchmarks: 2x Faster Suite

Before (500 tests): python manage.py test → 45s
After pytest: pytest -n4 → 22s (xdist parallel)

# tox.ini
[testenv]
deps=pytest pytest-django pytest-xdist
commands=pytest -n auto --reuse-db {posargs}

Full Migration Checklist

  1. pip uninstall django-test-runner (if any)
  2. pyproject.toml: pytest = "^8.0", pytest-django = "^5.0"
  3. Move tests.pytests/test_*.py
  4. Replace TestCase → def test_*(fixtures)
  5. manage.py testpytest
  6. TOXENV=test tox

Deploy Today

Migrate incrementally: pytest tests/views/ first. Share your patterns!

<RelatedLinks {relatedLinks} />

Sponsored by Durable Programming

Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.

Hire Durable Programming