Your Sunday, upgraded. Free live AI class · 5 PM ISTReserve your seat
PROGRAMMING LANGUAGES

Testing a Python Project with pytest: Fixtures, Mocks and Coverage That Matters

A good test suite lets you change code with confidence. This guide sets up pytest for a small Python service and shows how to structure tests, share setup with fixtures, isolate external services and use coverage to find gaps rather than chase a number.

On this page

Set up the project

Install pytest in the project's virtual environment and keep tests in a tests directory that mirrors the package structure, such as tests/test_pricing.py for app/pricing.py. Configure pytest in pyproject.toml so everyone runs it the same way, for example by setting the test paths and enabling strict markers. Run pytest from the project root; a single command should run the whole suite.

Write tests for behaviour

Each test should check one behaviour and be named after it, such as test_student_discount_applies_once. Structure the body as arrange, act and assert. Plain assert statements are enough, because pytest rewrites them to show the compared values on failure. Prefer testing public functions and observable results over internal helpers, so refactoring does not break tests unnecessarily.

@pytest.mark.parametrize(
    "price, coupon, expected",
    [(399, None, 399), (399, "STUDENT20", 319), (99, "EXPIRED", 99)],
    ids=["no-coupon", "student", "expired"],
)
def test_final_price(price, coupon, expected):
    assert final_price(price, coupon) == expected

Share setup with fixtures

Fixtures provide reusable setup, such as a test database session or a sample course, and pytest injects them by parameter name. Define shared fixtures in conftest.py. Choose the scope deliberately: function scope gives each test a fresh object, while module or session scope saves time for expensive resources but risks tests affecting each other. Use yield fixtures to clean up after the test.

Keep the values that matter to a test visible in the test itself. A fixture that hides an important input makes the test harder to understand.

Isolate external services

Tests should not call real payment providers, email services or paid APIs. Replace them at the boundary: inject a client object your code depends on and pass a fake in tests, or patch it with pytest's monkeypatch or unittest.mock. Mock the boundary your code owns, not deep internals of a third-party library, so tests remain stable when the library changes. For HTTP clients, response-mocking libraries can simulate status codes, timeouts and malformed bodies.

  • Test success responses and each kind of failure you handle.
  • Assert on what your code did with the response, not only that the mock was called.
  • Keep a few integration tests against real services in a separate, opt-in suite where appropriate.

Test with a real database where it matters

Mocking a database often hides the bugs that matter, such as constraint violations and query mistakes. For data-access code, run tests against the same database engine as production, for example in a container, and wrap each test in a transaction that is rolled back, or recreate the schema per test session. These tests are slower, so keep them focused on queries and repositories.

Use coverage to find gaps

Measure coverage with pytest-cov and read the report for untested branches in important modules, such as pricing, permissions and payment handling. A high overall percentage can still miss the riskiest paths, and chasing 100% often produces tests that assert nothing useful. Set a sensible minimum in CI to prevent coverage from dropping, and review uncovered lines in critical code during review.

Keep the suite fast and reliable

  • Run unit tests in seconds; mark slow tests and run them separately if needed.
  • Avoid sleeping in tests; control time with injectable clocks or freezing libraries.
  • Make tests independent so they pass in any order and in parallel.
  • Fix or quarantine flaky tests immediately; an unreliable suite gets ignored.

Checklist

  • Tests mirror the package structure and run with one command
  • Each test checks one named behaviour
  • Fixtures share setup with deliberate scope and cleanup
  • External services are replaced at your own boundary
  • Data-access code is tested against a real database engine
  • Coverage guides attention to critical untested paths

Share or cite this guide

Innovate with Ajay Editorial (2026-10-07). Testing a Python Project with pytest: Fixtures, Mocks and Coverage That Matters. https://innovatewithajay.com/articles/testing-a-python-project-with-pytest

Editorial approach, sources and corrections

YOUR NEXT CHAPTER

You have the curiosity.
Let’s turn it into capability.

Start with a practical workshop or bring a specific question to Ajay.

Chat with Ajay