← Back to list

Stop Repeating Yourself in Tests: A Clear Guide to Fixtures in Python

Fixtures may look simple — but used properly, they can completely change how you design your test suite.

Moh Haziane in Python in Plain English · 2026-05-10 15:09 · 43 claps · 4.6 min read paywalled
#python #testing #pytest #programming #software-development
Open on Medium ↗
Wiki topics: 💻 · Programming

Stop Repeating Yourself in Tests: A Clear Guide to Fixtures in Python

Fixtures may look simple — but used properly, they can completely change how you design your test suite.

AI generated

AI generated

Most developers learn pytest fixtures early — but many never realize how powerful they become once a project grows.

When you first start writing tests in Python, things usually feel straightforward. A few test functions, a few assertions, and everything works nicely.

But as the project grows, tests begin to require more setup: a database connection, a configured API client, prepared test data, or temporary files. Soon, you notice the same setup code appearing in multiple tests.

This repetition makes tests harder to maintain and more fragile.

If you’re using pytest, there's a clean solution to this problem: fixtures.

In this article, we’ll explore what fixtures are, why they exist, and how to use them effectively to keep your test suite clean and maintainable.

The Problem: Repeated Test Setup

Let’s start with something simple.

def add(a, b):
    return a + b

A basic test might look like this:

def test_add():
    assert add(5, 3) == 8

Easy.

Now imagine a slightly more realistic example.

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def is_adult(self):
        return self.age >= 18

Without fixtures, your tests might look like this:

def test_user_is_adult():
    user = User("Alice", 20)
    assert user.is_adult() is True

def test_user_is_not_adult():
    user = User("Bob", 15)
    assert user.is_adult() is False

Still manageable.

But imagine the User object now requires:

  • database access
  • password hashing
  • an external service
  • multiple configuration steps

Copy-pasting setup code across tests quickly becomes messy.

This is exactly the problem fixtures solve.

What Is a Fixture?

A fixture is a reusable setup function for your tests.

In pytest, a fixture:

  • prepares the environment needed by a test
  • provides data or resources to that test
  • can optionally perform cleanup afterward

You can think of a fixture as a helper that prepares something your test needs and provides it automatically.

Your First Fixture

Let’s rewrite the previous example using fixtures.

import pytest

@pytest.fixture
def adult_user():
    return User("Alice", 20)
@pytest.fixture
def minor_user():
    return User("Bob", 15)

Now the tests become simpler:

def test_user_is_adult(adult_user):
    assert adult_user.is_adult() is True

def test_user_is_not_adult(minor_user):
    assert minor_user.is_adult() is False

I highly recommend running these examples locally to really see how pytest executes fixtures and injects them into your tests.

Notice something interesting: we never call adult_user() ourselves.

Instead, pytest:

  1. sees that the test requires adult_user
  2. runs the fixture
  3. injects the returned value into the test function

This mechanism is known as dependency injection, and it’s one of the reasons pytest feels so elegant.

How Fixture Injection Works

When pytest encounters a test like this:

def test_something(my_fixture):

It searches for a fixture named my_fixture.

If it finds one, pytest:

  1. executes the fixture function
  2. takes its return value
  3. passes that value as an argument to the test

This automatic wiring allows tests to stay short and expressive.

Fixtures Can Depend on Other Fixtures

One powerful feature of pytest is that fixtures can depend on other fixtures.

For example:

@pytest.fixture
def user():
    return {"name": "Alice"}

@pytest.fixture
def authenticated_user(user):
    user["authenticated"] = True
    return user

Now any test that requests authenticated_user will automatically receive the result of the user fixture as well:

def test_auth(authenticated_user):
    assert authenticated_user["authenticated"] is True

Pytest automatically resolves this dependency chain for you.

This allows you to build small, reusable pieces of setup and compose them into more complex scenarios.

Setup and Teardown with yield

Many real-world test setups require cleanup.

For example:

  • opening a file and closing it after the test
  • connecting to a database and disconnecting later
  • starting a temporary server and shutting it down afterward

Fixtures support this pattern using yield.

@pytest.fixture
def temp_file():
    f = open("test.txt", "w")
    yield f
    f.close()

The fixture works in two stages:

  1. Code before yield prepares the resource.
  2. The value produced by yield is provided to the test.
  3. Code after yield runs once the test finishes and performs cleanup.

Example usage

def test_write_to_file(temp_file):
    temp_file.write("Hello tests!")

Here is what happens during execution:

  1. The fixture opens test.txt.
  2. The file object is passed to the test.
  3. The test writes data to the file.
  4. After the test finishes, the fixture closes the file automatically.

This pattern makes resource management both clear and safe.

Controlling How Often Fixtures Run (Scope)

By default, fixtures run once per test function.

However, pytest allows you to control this behavior using scopes.

@pytest.fixture(scope="module")
def resource():
    print("Setting up resource")
    return {}

Available scopes include:

  • function (default): runs for every test
  • class: runs once per test class
  • module: runs once per test file
  • session: runs once for the entire test session

Scopes are especially useful for expensive operations like:

  • creating a test database
  • loading large datasets
  • establishing external connections

Fixtures vs. setUp() in unittest

If you’ve used Python’s built-in unittest framework, you may be familiar with setUp():

def setUp(self):
    ...

Pytest fixtures offer several advantages:

  • reusable across multiple test files
  • support dependency injection
  • fixtures can depend on other fixtures
  • flexible scoping options

This flexibility is one reason many developers prefer pytest for modern Python projects.

A Real-World Use Case

In web applications such as Django, Flask, or FastAPI, fixtures are commonly used to:

  • create test users
  • provide an API client
  • set up a test database
  • insert sample records

Example:

from fastapi.testclient import TestClient
from my_app import app  # your FastAPI application instance

@pytest.fixture
def client():
    return TestClient(app)

Here, app is your web application instance (for example, a FastAPI or Flask app). The fixture creates a test client that allows your tests to send HTTP requests to the application without running a real server.

Tests that need the client simply declare it:

def test_homepage(client):
    response = client.get("/")
    assert response.status_code == 200

Clean, reusable, and scalable.

When Should You Use Fixtures?

Fixtures are helpful when:

  • you repeat setup logic across tests
  • setup becomes complex
  • cleanup is required after tests
  • you want isolated, maintainable tests

However, avoid introducing fixtures when setup is trivial. Simplicity should always remain the priority.

A Simple Mental Model

A useful mental model is:

A fixture prepares something your test needs and provides it automatically.

Or even simpler:

Fixtures give tests what they need.

Why Fixtures Matter in Large Test Suites

In small examples, fixtures may look like a convenience feature.

In large projects, they become essential.

Without fixtures, test suites often suffer from:

  • duplicated setup logic
  • fragile tests that break when setup changes
  • hard-to-maintain test files

Fixtures solve this by centralizing setup logic and keeping tests focused on behavior rather than preparation.

Final Thoughts

Fixtures aren’t just a convenience — they change how you think about testing. Instead of focusing on setup, you focus on behavior.

In practice, this means:

  • less duplicated code
  • clearer, more focused tests
  • safer handling of resources
  • easier scaling as your project grows

If you’re using pytest without taking advantage of fixtures, you're missing one of its most powerful tools.

Once you start thinking in terms of fixtures, your tests stop feeling like small scripts — and start behaving like well-designed systems.


메타데이터
post_id
b480a053e93b
slug
stop-repeating-yourself-in-tests-a-clear-guide-to-fixtures-in-python-b480a053e93b
url
https://python.plainenglish.io/stop-repeating-yourself-in-tests-a-clear-guide-to-fixtures-in-python-b480a053e93b
canonical_url
https://python.plainenglish.io/stop-repeating-yourself-in-tests-a-clear-guide-to-fixtures-in-python-b480a053e93b
author_url
https://medium.com/@bytehaven
status
ok
fetched_at
2026-06-09 15:37:30