Recap
In Article 1 we mapped the testing landscape: the test pyramid, the difference between unit, integration, and end-to-end tests, and the properties that make tests worth having.
In Article 2 we built a full unit test suite using Django's built-in TestCase. We tested model methods, properties, validators, custom managers, utility functions, and service layer logic. We covered setUp, setUpTestData, assertRaises, refresh_from_db, and @patch.
That suite works. It catches bugs, runs fast, and follows good practices. But it is written in the unittest style, which is verbose, requires class-based structure even when that structure adds nothing, and produces failure output that makes you do extra work to understand what went wrong.
In this article we take every test from Article 2 and migrate it to pytest. You will see each test before and after, understand exactly what changed and why, and come away with a pytest setup you can drop into any Django project.
Why pytest
pytest is a testing framework for Python that has become the de facto standard across the ecosystem. It is compatible with existing unittest.TestCase tests, so you can adopt it incrementally without rewriting anything.
The advantages over plain unittest are concrete and visible from the first test you write.
Better failure output
When a unittest assertion fails, you see the two values and the word "not equal." When a pytest assertion fails, you see the full expression that produced the wrong value, with the subexpressions expanded. This difference is small for simple assertions and enormous for complex ones.
# unittest failure output
AssertionError: Decimal('100.16') != Decimal('116.00')
# pytest failure output
AssertionError: assert Decimal('100.16') == Decimal('116.00')
where Decimal('100.16') = <Order subtotal=Decimal('100.00')>.totalNo class required
pytest discovers and runs plain functions whose names start with test_. You do not need to subclass anything. Classes are still available and useful for grouping related tests, but they are optional and require no inheritance.
Fixtures instead of setUp
pytest fixtures are functions that create and return test dependencies. Instead of one monolithic setUp that creates everything a class might need, fixtures are composable: each test declares exactly the dependencies it needs, and pytest injects them. This makes tests self-documenting and avoids creating objects that most tests in a class never use.
Parametrize
@pytest.mark.parametrize lets you run the same test logic against multiple inputs without duplicating the test function. In unittest you either write separate test methods for each input (tedious) or loop inside a test method (which hides failures behind a single test name).
Installation and configuration
Install pytest and the Django plugin:
pip install pytest pytest-djangopytest-django handles all the Django-specific setup: creating the test database, applying migrations, wrapping tests in transactions, and giving you Django's test utilities inside pytest tests.
Create a pytest.ini (or add a [tool.pytest.ini_options] section to your pyproject.toml) at the root of your project:
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = test_*.py
python_classes = Test*
python_functions = test_*If you use pyproject.toml:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings"
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]That is all the configuration you need. Run the suite with:
pytest✦ Tip
pytest discovers tests automatically
pytest walks your project directory and collects every file matching python_files, every class matching python_classes inside those files, and every function matching python_functions. You do not need to register test modules anywhere.
Your first migration
Let us start with the simplest possible migration: a test for Product.is_in_stock(). Here is the original unittest version:
from django.test import TestCase
from decimal import Decimal
from orders.models import Product
class ProductIsInStockTest(TestCase):
def test_returns_true_when_stock_is_positive(self):
product = Product(name="Widget", price=Decimal("10.00"), stock=5)
self.assertTrue(product.is_in_stock())
def test_returns_false_when_stock_is_zero(self):
product = Product(name="Widget", price=Decimal("10.00"), stock=0)
self.assertFalse(product.is_in_stock())
def test_returns_false_when_stock_is_not_set(self):
product = Product(name="Widget", price=Decimal("10.00"))
self.assertFalse(product.is_in_stock())import pytest
from decimal import Decimal
from orders.models import Product
@pytest.mark.django_db
def test_product_is_in_stock_when_stock_is_positive():
product = Product(name="Widget", price=Decimal("10.00"), stock=5)
assert product.is_in_stock()
@pytest.mark.django_db
def test_product_is_not_in_stock_when_stock_is_zero():
product = Product(name="Widget", price=Decimal("10.00"), stock=0)
assert not product.is_in_stock()
@pytest.mark.django_db
def test_product_is_not_in_stock_when_stock_is_not_set():
product = Product(name="Widget", price=Decimal("10.00"))
assert not product.is_in_stock()What changed:
self.assertTrue(x)becomesassert x. Plain Python. No method to memorise.- No class inheritance. Each test is a standalone function.
@pytest.mark.django_dbtells pytest-django that this test may access the database. Without it, any database access raises an error. This is a feature: it forces you to be explicit about which tests touch the database, making the ones that do not (pure logic tests) clearly faster.
Notice that these tests do not actually hit the database. Product(...) creates an unsaved instance. The @pytest.mark.django_db marker is technically unnecessary here, but it is a good habit to add it whenever the model under test could plausibly need the database, and it costs nothing.
✦ Tip
Skip @pytest.mark.django_db for pure logic tests
If a test only creates unsaved model instances and calls methods that do not touch the database, you can omit @pytest.mark.django_db entirely. pytest-django will run the test faster because it does not set up the database transaction wrapper. Tests that try to access the database without the marker will fail with a clear error, which is the intended behaviour.
pytest fixtures
Fixtures are the most important concept to understand in pytest. A fixture is a function decorated with @pytest.fixture that creates and returns a value. Any test function (or other fixture) that declares a parameter with the same name as a fixture will receive that value automatically.
import pytest
from decimal import Decimal
from orders.models import Customer, Product, Order
@pytest.fixture
def customer(db):
return Customer.objects.create(name="Alice", email="alice@example.com")
@pytest.fixture
def product(db):
return Product.objects.create(name="Widget", price=Decimal("25.00"), stock=10)
@pytest.fixture
def order(db, customer):
return Order.objects.create(customer=customer)The db fixture is provided by pytest-django and grants access to the database for that test. When a fixture depends on db, any test that uses that fixture automatically gets database access too, without needing @pytest.mark.django_db.
Fixtures compose naturally. The order fixture depends on customer, which depends on db. pytest resolves the dependency chain and injects everything in the right order.
A test that uses these fixtures looks like this:
def test_add_item_increases_order_subtotal(order, product):
from orders.services import add_item_to_order
add_item_to_order(order, product, quantity=2)
order.refresh_from_db()
assert order.subtotal == Decimal("50.00")The test declares the two things it needs in its parameter list. pytest reads the parameter names, finds the matching fixtures, runs them, and passes the results in. No setUp. No self. No inheritance.
Compare this to the unittest equivalent. A test class that needs customer, product, and order must create all three in setUp, even if individual test methods only use one or two of them. Fixtures solve this: each test gets exactly what it asks for.
Fixture scopes
By default a fixture runs once per test function. This is the safest behaviour: each test gets a fresh object with no state carried over from previous tests.
For expensive setup that you want to share across multiple tests, you can change the fixture scope:
import pytest
# Runs once per test function (default)
@pytest.fixture(scope="function")
def product(db):
return Product.objects.create(name="Widget", price=Decimal("25.00"), stock=10)
# Runs once per test class
@pytest.fixture(scope="class")
def customer(db):
return Customer.objects.create(name="Alice", email="alice@example.com")
# Runs once per test module (file)
@pytest.fixture(scope="module")
def expensive_setup(django_db_setup):
...
# Runs once for the entire test session
@pytest.fixture(scope="session")
def very_expensive_setup(django_db_setup):
...⚠ Gotcha
Wider scope means more risk of shared state bugs
A session-scoped fixture is created once and shared across every test in the session. If any test modifies the fixture object, subsequent tests see the modified state. Use function scope (the default) unless you have a measurable performance reason to widen it, and never mutate shared fixtures.
For database fixtures specifically, pytest-django provides db (function scope, wraps each test in a transaction) and django_db_setup (session scope, for fixtures that need to persist across tests). The db fixture is almost always what you want.
conftest.py
Fixtures defined inside a test file are only available to tests in that file. To share fixtures across multiple test files, put them in a conftest.py file. pytest automatically discovers and loads conftest.py files as it walks the directory tree.
orders/
└── tests/
├── conftest.py # fixtures available to all tests in this directory
├── test_models.py
├── test_services.py
└── test_utils.pyimport pytest
from decimal import Decimal
from django.utils import timezone
from datetime import timedelta
from orders.models import Customer, Product, Coupon, Order
@pytest.fixture
def customer(db):
return Customer.objects.create(
name="Alice",
email="alice@example.com",
is_loyalty_member=False,
)
@pytest.fixture
def loyalty_customer(db):
return Customer.objects.create(
name="Bob",
email="bob@example.com",
is_loyalty_member=True,
)
@pytest.fixture
def product(db):
return Product.objects.create(
name="Widget",
price=Decimal("25.00"),
stock=10,
)
@pytest.fixture
def order(db, customer):
return Order.objects.create(customer=customer)
@pytest.fixture
def valid_coupon(db):
return Coupon.objects.create(
code="SAVE10",
discount_percent=10,
expires_at=timezone.now() + timedelta(days=7),
)
@pytest.fixture
def expired_coupon(db):
return Coupon.objects.create(
code="OLD10",
discount_percent=10,
expires_at=timezone.now() - timedelta(seconds=1),
)Every test file inside orders/tests/ can now use any of these fixtures by declaring them as parameters. You define the objects once and compose them freely.
Migrating model tests
Here is the full migration of the model test suite from Article 2. Each test class becomes a group of plain functions, and setUp is replaced by fixture parameters.
import pytest
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import timedelta
from freezegun import freeze_time
from orders.models import Product, Coupon, Order, Customer
# Product.is_in_stock
def test_product_is_in_stock_when_stock_is_positive():
product = Product(name="Widget", price=Decimal("10.00"), stock=5)
assert product.is_in_stock()
def test_product_is_not_in_stock_when_stock_is_zero():
product = Product(name="Widget", price=Decimal("10.00"), stock=0)
assert not product.is_in_stock()
def test_product_is_not_in_stock_when_stock_is_not_set():
product = Product(name="Widget", price=Decimal("10.00"))
assert not product.is_in_stock()
# Coupon.is_valid
@freeze_time("2026-06-01 12:00:00")
def test_coupon_is_valid_before_expiry():
coupon = Coupon(
code="SAVE10",
discount_percent=10,
expires_at=timezone.now() + timedelta(hours=1),
)
assert coupon.is_valid()
@freeze_time("2026-06-01 12:00:00")
def test_coupon_is_not_valid_after_expiry():
coupon = Coupon(
code="SAVE10",
discount_percent=10,
expires_at=timezone.now() - timedelta(hours=1),
)
assert not coupon.is_valid()
# Coupon.clean
def test_coupon_clean_raises_for_discount_above_100():
coupon = Coupon(
code="INVALID",
discount_percent=150,
expires_at=timezone.now() + timedelta(days=7),
)
with pytest.raises(ValidationError):
coupon.clean()
def test_coupon_clean_accepts_discount_of_100():
coupon = Coupon(
code="ALLFREE",
discount_percent=100,
expires_at=timezone.now() + timedelta(days=7),
)
coupon.clean() # should not raise
def test_coupon_clean_accepts_discount_of_zero():
coupon = Coupon(
code="NODISCOUNT",
discount_percent=0,
expires_at=timezone.now() + timedelta(days=7),
)
coupon.clean() # should not raise
# Order.total
def test_order_total_applies_tax_to_subtotal():
order = Order(subtotal=Decimal("100.00"), tax_rate=Decimal("0.16"))
assert order.total == Decimal("116.00")
def test_order_total_is_zero_when_subtotal_is_zero():
order = Order(subtotal=Decimal("0.00"), tax_rate=Decimal("0.16"))
assert order.total == Decimal("0.00")
def test_order_total_uses_actual_tax_rate_not_default():
order = Order(subtotal=Decimal("100.00"), tax_rate=Decimal("0.20"))
assert order.total == Decimal("120.00")
# Order.is_cancellable
def test_pending_order_is_cancellable():
assert Order(status="pending").is_cancellable
def test_confirmed_order_is_cancellable():
assert Order(status="confirmed").is_cancellable
def test_shipped_order_is_not_cancellable():
assert not Order(status="shipped").is_cancellable
def test_cancelled_order_is_not_cancellable():
assert not Order(status="cancelled").is_cancellable
# OrderManager
@pytest.mark.django_db
def test_pending_manager_returns_only_pending_orders(customer):
Order.objects.create(customer=customer, status="pending")
Order.objects.create(customer=customer, status="confirmed")
Order.objects.create(customer=customer, status="shipped")
result = Order.objects.pending()
assert result.count() == 1
assert result.first().status == "pending"
@pytest.mark.django_db
def test_pending_manager_returns_empty_when_no_pending_orders(customer):
Order.objects.create(customer=customer, status="confirmed")
assert Order.objects.pending().count() == 0
@pytest.mark.django_db
def test_for_customer_manager_returns_only_that_customers_orders(customer, db):
other_customer = Customer.objects.create(
name="Bob", email="bob@example.com"
)
Order.objects.create(customer=customer, status="pending")
Order.objects.create(customer=customer, status="confirmed")
Order.objects.create(customer=other_customer, status="pending")
result = Order.objects.for_customer(customer)
assert result.count() == 2
assert all(o.customer == customer for o in result)
@pytest.mark.django_db
def test_for_customer_manager_returns_empty_when_customer_has_no_orders(customer):
assert Order.objects.for_customer(customer).count() == 0Several things are worth highlighting in this migration.
The tests for unsaved model instances (everything before the manager tests) have no database marker at all. They are pure Python function calls. pytest runs them with no transaction overhead, no database setup, nothing. They are as fast as it is possible for a test to be.
The manager tests use the customer fixture from conftest.py by simply declaring it as a parameter. pytest finds the fixture, runs it, and passes the result in. The test body is clean: it only contains the logic specific to that scenario.
pytest.raises is the pytest equivalent of assertRaises. It works as a context manager in the same way.
Migrating service tests
import pytest
from decimal import Decimal
from django.core.exceptions import ValidationError
from orders.models import Order
from orders.services import add_item_to_order, cancel_order
@pytest.mark.django_db
def test_add_item_increases_order_subtotal(order, product):
add_item_to_order(order, product, quantity=2)
order.refresh_from_db()
assert order.subtotal == Decimal("50.00")
@pytest.mark.django_db
def test_add_item_reduces_product_stock(order, product):
add_item_to_order(order, product, quantity=3)
product.refresh_from_db()
assert product.stock == 7
@pytest.mark.django_db
def test_add_item_raises_for_zero_quantity(order, product):
with pytest.raises(ValueError):
add_item_to_order(order, product, quantity=0)
@pytest.mark.django_db
def test_add_item_raises_for_negative_quantity(order, product):
with pytest.raises(ValueError):
add_item_to_order(order, product, quantity=-1)
@pytest.mark.django_db
def test_add_item_raises_when_quantity_exceeds_stock(order, product):
with pytest.raises(ValidationError):
add_item_to_order(order, product, quantity=11)
@pytest.mark.django_db
def test_add_item_does_not_change_stock_on_failure(order, product):
try:
add_item_to_order(order, product, quantity=11)
except ValidationError:
pass
product.refresh_from_db()
assert product.stock == 10
@pytest.mark.django_db
def test_cancel_order_sets_status_to_cancelled_for_pending_order(customer):
order = Order.objects.create(customer=customer, status="pending")
cancel_order(order)
order.refresh_from_db()
assert order.status == "cancelled"
@pytest.mark.django_db
def test_cancel_order_sets_status_to_cancelled_for_confirmed_order(customer):
order = Order.objects.create(customer=customer, status="confirmed")
cancel_order(order)
order.refresh_from_db()
assert order.status == "cancelled"
@pytest.mark.django_db
def test_cancel_order_raises_for_shipped_order(customer):
order = Order.objects.create(customer=customer, status="shipped")
with pytest.raises(ValueError):
cancel_order(order)
@pytest.mark.django_db
def test_cancel_order_does_not_change_status_on_failure(customer):
order = Order.objects.create(customer=customer, status="shipped")
try:
cancel_order(order)
except ValueError:
pass
order.refresh_from_db()
assert order.status == "shipped"The service tests use fixtures from conftest.py directly. No setUp method. No class. Each test declares the objects it needs and receives them ready to use.
Notice that test_cancel_order_sets_status_to_cancelled_for_pending_order does not use the order fixture from conftest.py. That fixture creates an order with the default status, but this test needs to control the status explicitly. Creating the order directly inside the test body is perfectly fine when the fixture does not match the specific scenario.
Migrating utility tests
from decimal import Decimal
from orders.utils import calculate_loyalty_discount, format_currency, is_bulk_order
def test_loyalty_member_gets_ten_percent_off():
result = calculate_loyalty_discount(Decimal("100.00"), is_loyalty_member=True)
assert result == Decimal("90.00")
def test_non_member_gets_no_discount():
result = calculate_loyalty_discount(Decimal("100.00"), is_loyalty_member=False)
assert result == Decimal("100.00")
def test_loyalty_discount_applies_to_any_amount():
result = calculate_loyalty_discount(Decimal("250.00"), is_loyalty_member=True)
assert result == Decimal("225.00")
def test_loyalty_discount_on_zero_subtotal_stays_zero():
result = calculate_loyalty_discount(Decimal("0.00"), is_loyalty_member=True)
assert result == Decimal("0.00")
def test_format_currency_uses_kes_by_default():
assert format_currency(Decimal("1234.50")) == "KES 1,234.50"
def test_format_currency_uses_custom_currency():
assert format_currency(Decimal("1234.50"), currency="USD") == "USD 1,234.50"
def test_format_currency_formats_zero():
assert format_currency(Decimal("0.00")) == "KES 0.00"
def test_is_bulk_order_returns_true_at_default_threshold():
assert is_bulk_order(10)
def test_is_bulk_order_returns_true_above_threshold():
assert is_bulk_order(50)
def test_is_bulk_order_returns_false_below_threshold():
assert not is_bulk_order(9)
def test_is_bulk_order_returns_false_at_zero():
assert not is_bulk_order(0)
def test_is_bulk_order_respects_custom_threshold():
assert is_bulk_order(5, threshold=5)
assert not is_bulk_order(4, threshold=5)These tests have no imports from django.test, no markers, and no fixtures. They are plain Python functions that call Python functions. They run in milliseconds and produce crystal-clear failure output.
Markers
Markers are labels you attach to tests. pytest uses them to filter which tests to run and to apply behaviours. You have already used @pytest.mark.django_db. Here are the other markers you will reach for regularly.
skip and skipif
import pytest
@pytest.mark.skip(reason="payment gateway not yet implemented")
def test_charge_customer():
...
@pytest.mark.skipif(
condition=not settings.STRIPE_ENABLED,
reason="Stripe is disabled in this environment",
)
def test_stripe_webhook():
...xfail
xfail marks a test that is expected to fail. The test runs, and if it fails, pytest counts it as an expected failure (xfail) rather than a real failure. If it unexpectedly passes, pytest counts it as xpass, which you can configure to be an error.
@pytest.mark.xfail(reason="known bug in discount rounding, tracked in issue #42")
def test_discount_rounding_edge_case():
result = calculate_loyalty_discount(Decimal("0.01"), is_loyalty_member=True)
assert result == Decimal("0.009")Custom markers
You can define your own markers to categorise tests and run subsets selectively. Register them in pytest.ini to avoid warnings:
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
markers =
slow: marks tests as slow (run with -m slow)
integration: marks integration tests@pytest.mark.slow
@pytest.mark.django_db
def test_large_order_processing():
...# Run only slow tests
pytest -m slow
# Run everything except slow tests
pytest -m "not slow"
# Run slow integration tests
pytest -m "slow and integration"Parametrize
@pytest.mark.parametrize lets you run the same test logic with different inputs. Instead of writing a separate test function for each input combination, you write one function and supply a list of cases.
Look at the is_cancellable tests. In unittest we wrote four methods that all had the same structure. With parametrize:
def test_pending_order_is_cancellable():
assert Order(status="pending").is_cancellable
def test_confirmed_order_is_cancellable():
assert Order(status="confirmed").is_cancellable
def test_shipped_order_is_not_cancellable():
assert not Order(status="shipped").is_cancellable
def test_cancelled_order_is_not_cancellable():
assert not Order(status="cancelled").is_cancellableimport pytest
from orders.models import Order
@pytest.mark.parametrize("status,expected", [
("pending", True),
("confirmed", True),
("shipped", False),
("cancelled", False),
])
def test_order_is_cancellable(status, expected):
assert Order(status=status).is_cancellable == expectedpytest runs this function four times, once for each row in the list, and labels each run with the parameter values so you can see exactly which case failed:
test_models.py::test_order_is_cancellable[pending-True] PASSED
test_models.py::test_order_is_cancellable[confirmed-True] PASSED
test_models.py::test_order_is_cancellable[shipped-False] PASSED
test_models.py::test_order_is_cancellable[cancelled-False] PASSEDWhen to use parametrize and when to use separate functions is a judgement call. Use parametrize when the only thing that varies between tests is the input data and the expected output. Use separate functions when the setup or assertions differ meaningfully between cases, since squeezing them into a single parametrize call often makes the test harder to read.
# Parametrize also works with fixtures
@pytest.mark.parametrize("quantity,expected_error", [
(0, ValueError),
(-1, ValueError),
(11, ValidationError),
])
@pytest.mark.django_db
def test_add_item_raises_for_invalid_quantity(order, product, quantity, expected_error):
with pytest.raises(expected_error):
add_item_to_order(order, product, quantity=quantity)Mocking with pytest
pytest does not replace unittest.mock. It uses it. The standard approach is to use @patch from unittest.mock exactly as before. But pytest also supports the mocker fixture from pytest-mock, which some teams prefer because it removes the need for decorators.
pip install pytest-mockfrom unittest.mock import patch
@patch("orders.services.payment_gateway.charge")
@pytest.mark.django_db
def test_charge_is_called_with_correct_amount(mock_charge, order):
mock_charge.return_value = {"status": "success", "charge_id": "ch_123"}
from orders.services import charge_customer
charge_customer(order_id=order.pk, amount="100.00")
mock_charge.assert_called_once_with("100.00")@pytest.mark.django_db
def test_charge_is_called_with_correct_amount(mocker, order):
mock_charge = mocker.patch("orders.services.payment_gateway.charge")
mock_charge.return_value = {"status": "success", "charge_id": "ch_123"}
from orders.services import charge_customer
charge_customer(order_id=order.pk, amount="100.00")
mock_charge.assert_called_once_with("100.00")The mocker fixture automatically stops patches after each test, so there is no risk of a mock leaking into another test. It also composes naturally with other fixtures, which the decorator-based approach does not.
Running tests
The pytest CLI gives you precise control over which tests to run and how.
# Run the full suite
pytest
# Run a specific file
pytest orders/tests/test_models.py
# Run a specific test function
pytest orders/tests/test_models.py::test_product_is_in_stock_when_stock_is_positive
# Run tests matching a keyword in their name
pytest -k "coupon"
# Run tests with a specific marker
pytest -m django_db
# Stop after the first failure
pytest -x
# Show local variables in failure output
pytest -l
# Rerun only the tests that failed in the last run
pytest --lf
# Rerun failures first, then the rest
pytest --ff
# Show a summary of all passed, failed, and skipped tests
pytest -v
# Reuse the existing test database (skip migrations)
pytest --reuse-db✦ Tip
Use --reuse-db during active development
pytest-django recreates the test database on every run by default. --reuse-db skips that step and reuses the existing database, which can save several seconds on projects with many migrations. When you add a new migration, drop the flag once and pytest-django will rebuild the database.
The flags you will reach for most often are -x (stop on first failure), -k (run a subset by name), and --lf (rerun only failures). Together they give you a tight feedback loop: run the suite, see what failed, run only those tests while you fix them, then run the full suite to confirm nothing regressed.
Summary
- pytest is compatible with existing
unittest.TestCasetests. You can adopt it incrementally. - Configure it with a
pytest.inifile and setDJANGO_SETTINGS_MODULE. That is all the setup required. - Use
@pytest.mark.django_dbon any test that accesses the database. Omit it for pure logic tests and they will run faster. - Fixtures replace
setUp. They are composable, reusable functions defined inconftest.pyand injected via parameter names. - The
dbfixture from pytest-django grants database access and handles transaction rollback between tests. - Use
scope="function"(the default) for database fixtures unless you have a clear performance reason to widen the scope. pytest.raisesreplacesassertRaises.assertreplaces everyself.assert*method.@pytest.mark.parametrizeeliminates duplicated test functions that vary only in their inputs.pytest-mock'smockerfixture patches automatically without needing decorators ortearDown.- The most useful CLI flags are
-x,-k,--lf, and--reuse-db.
In the next article we move up the test pyramid to integration tests. We will test Django views end-to-end using TestClient and Django REST Framework's APIClient, covering status codes, response bodies, serializer validation, and URL routing.