Unit Testing in Django with Pytest: A Practical Guide

Published On: 23 June 2025.By .
  • General
  • Quality Engineering

Unit testing isn’t just about catching bugs — it’s a mindset that helps you write cleaner, more maintainable, and bug-resistant code from the start. When building Django applications, adopting pytest with proper patterns like fixtures, mocks, and factories makes testing powerful yet elegant.

In this blog, we’ll walk through:

  • Why testing matters
  • Pytest fundamentals
  • Practical tools: conftest.py, fixtures, RequestFactory, mocking
  • How to write clean, reusable, and scalable test cases

🔍 Why Is Testing Important?

Good testing ensures:

  1. Code Quality – Helps you write robust and predictable code.
  2. Facilitates Refactoring – Safe refactoring with test coverage.
  3. Saves Time and Cost – Detects issues early.
  4. Encourages Better Design – Forces you to modularize.
  5. Prevents Regressions – Alerts you when something breaks unexpectedly.

🧱 Types of Tests

  • Unit Tests: Test individual components, such as functions and models.
  • Integration Tests: Validate interactions between modules or systems.
  • Functional Tests: Simulate user-level operations, ensuring the whole system works.

We’ll focus on unit tests here using pytest in a Django context.

⚙️ Pytest Setup in Django

Install dependencies:

In your project root, create or update pytest.ini:

Now you’re ready to test Django apps with Pytest!

🗂 Recommended Test Folder Structure

Inside each Django app, use a folder named tests/:

Writing a “good” test isn’t just about checking if something returns the expected result. It’s about structure, clarity, and isolation. Let’s walk through the key ingredients that make your Django tests powerful and maintainable using pytest.

Writing and Using Factories (Factory Boy)

What is a Factory?

In the context of testing, a factory is a helper object that generates model instances (or other Python objects) with default values. Instead of writing repetitive User.objects.create(…) or manually filling each field with test data, a factory automates this process and gives you complete control to override values when needed. Think of it like a blueprint for creating test data. Once defined, you can use it in your tests to quickly spin up realistic test objects without writing the same code over and over again. Factories are commonly implemented using the Factory Boy library in Django.

Why Use It?

  • Avoid writing User.objects.create(…) in every test
  • Generate fake but realistic data
  • Easily customize fields

Step-by-Step

In your app’s test folder, create a file:

📁 your_app/tests/factories.py

Here, we define default values for our model so we don’t need to pass them manually every time.

How to Use It

Example 1: Use it directly in a test

You can import and call this factory wherever you need a test user.

📁 test_models.py

Example 2: Customize fields

If you want to override some values while keeping the rest default:

Example 3: Create a batch of users

To generate multiple users for bulk testing:

Writing Fixtures

What is a Fixture?

In pytest, a fixture is a function that provides setup code that your test functions can use. Think of it like a preconfigured toolbox that runs before your test and prepares the environment or data needed for the test to run properly. Instead of repeating the same setup logic in every test, you define a fixture once and then simply reference it by name as a test function argument. Pytest automatically calls it and passes its result to your test.

Why Use Fixtures?

  • Reduce duplication
  • Keep tests clean
  • Centralized test setup logic

Step-by-Step

To define a reusable fixture, create a conftest.py file in the project root, where pytest will automatically discover it.

📁 conftest.py

To define a reusable fixture, create a conftest.py file in the project root (the same level as pytest.ini and manage.py). Pytest automatically discovers this file and makes any fixtures defined in it globally available to all te- st files without needing to import them manually.

How conftest.py works:

  • Any fixture declared here is automatically available to any test in your Django project.
  • You do not need to import the fixture — pytest handles this behind the scenes.
  • Useful for fixtures that are used across multiple apps or files (e.g., user factories, test settings, database connections).
  • Scoping: You can also control fixture scope using the @pytest.fixture(scope=”…”) argument. For example, scope=”session” will run the fixture only once per test session.

App-level conftest.py:

  • You can also define a conftest.py inside a specific Django app’s test folder to keep test setup modular.
  • In this case, fixtures in that file will only be available to tests within the same directory tree (i.e., within that app).
  • This is useful for app-specific test data or mocking logic.

Note: If a fixture is defined in both the root and app-level conftest.py, the app-level version will override it for tests in that app.

Here, we wrap the factory to return a fresh object every time it’s needed:

How to Use It

Fixtures in pytest can be directly injected into your test functions by just adding them as parameters.

📁 test_models.py

Here, test_user is automatically created by the fixture we defined in conftest.py.

Markers and Parametrization

Markers are special decorators in pytest that provide additional information about how tests should be executed or categorized. They help customize test behavior by allowing you to label tests with specific attributes, which can then be used to control their execution, reporting, and organization.

Common Uses of Markers:

1. Categorization:

Markers can be used to group tests into categories for easier identification, such as @pytest.mark.smoke, @pytest.mark.regression, or @pytest.mark.performance. This allows you to selectively run groups of tests based on their category.

Example:

You can then run only smoke tests with:

2. Conditional Execution:

Certain markers can specify conditions under which a test should be executed or skipped. For instance, @pytest.mark.skip can be used to skip a test, while @pytest.mark.skipif(condition) skips it only if a certain condition is met.

Example:

3. Parameterization:

Markers can facilitate parameterized testing, allowing a single test function to run with multiple sets of input data using @pytest.mark.parametrize. This is useful for testing the same functionality with different scenarios.

Example:

4. Custom Test Behavior:

Users can define their own markers to implement custom behaviors, such as tagging tests that require database access or need to run in a specific environment. By utilizing markers, you can achieve more organized, flexible, and efficient test management in your pytest test suite.

Example:

You must register custom markers in pytest.ini to avoid warnings:

Mocking External Services

Mocking involves substituting real external systems—such as APIs, files, and emails—with simulated objects. This allows for testing and development without relying on the actual external systems.

This is especially useful when:

  • The real system is slow (e.g., network call)
  • The real system has side effects (e.g., sending emails)
  • The real system is unavailable during tests (e.g., a third-party API)

By using mocking, you can control external dependencies, simulate different scenarios (e.g., success, failure, timeout), and make your tests fast, stable, and independent.

Step-by-Step: Creating and Using a Mock Function

Imagine you have a service function like this:

📁 your_app/services.py

Now let’s say you want to test a function that uses it:

📁 your_app/utils.py

To test this without actually sending an email, you mock send_email:

📁 your_app/tests/test_utils.py

Summary:

Note: When using @patchYou must mock the function where it is imported, not where it is defined.

For example:

  • If send_email is defined in your_app.services but used in your_app.utils, then you should patch your_app.utils.send_email, not your_app.services.send_email.
  • Always patch the location of usage, because that’s where Python looks it up during execution.
  • We used @patch to replace the real send_email with a mock.
  • We controlled its return value.
  • We verified whether it was called and with what arguments.

Here, the function mock_send_email is not created manually — it’s automatically provided by the @patch(…) decorator from unittest.mock. mock_send_email is the mock object injected by patch into the test function. So, you don’t create mock_send_email manually, the decorator handles it for you.

When using multiple @patch decorators, the order of the decorators determines the order of the arguments passed into the test function, and it’s reversed. Here’s how it works:

RequestFactory for View Testing

What is RequestFactory?

RequestFactory is a utility class provided by Django that allows you to create mock HTTP requests (like GET, POST, PUT, etc.) for unit testing views. It simulates requests without actually running the server or going through Django’s full request-response cycle.

This is different from Django’s Client, which performs an end-to-end request through URL routing, middleware, and template rendering. RequestFactory skips all that and directly targets the view logic.

Why Use It?

  • Test views in isolation: You can focus purely on view logic without worrying about middleware, routing, or template rendering.
  • Speed: Since it bypasses routing and middleware, it’s faster than using Django’s test Client.
  • Control: Allows you to customize request headers, methods, and data to simulate edge cases and complex requests.

Example:

Tips for Clean Tests

  • Test one thing at a time.
  • Use factories and fixtures to reduce redundancy.
  • Keep test names descriptive: test_user_login_fails_on_invalid_password.
  • Avoid hard-coded data—use faker, factories.
  • Organize tests per app or feature.

Testing is an investment. With Pytest + Django, you’re equipped with a framework that is:

  • Powerful (supports mocking, fixtures, DB, view tests)
  • Flexible (custom configuration via conftest.py)
  • Efficient (fast feedback loop)

Don’t test just for the sake of 100% coverage. Test for confidence, reliability, and maintainability.

📚 Further Reading and Official References

For more advanced usage and deep dives, check out the official resources:

Happy testing! No guessing. Just clean, reliable, maintainable tests.

Related content

That’s all for this blog

Go to Top