The Power of Pytest Fixtures: Setup Once, Test Everywhere.

Anil-Pytest-fixtures

If you've ever copied and pasted the same setup code across multiple test files—or found yourself avoiding tests that involve databases, APIs, or any kind of repetitive prep work—this post is for you.

Writing good tests isn’t just about making assertions. It's also about keeping your test code clean, avoiding repetition, and making things easy to read and maintain. That’s where Pytest fixtures come in and save the day.

What are Pytest Fixtures?

At its core, a Pytest fixture is a reusable function that sets up the context or data your tests need. Whether it's a simple set of numbers, a database connection, or a mock service, fixtures help keep your tests focused on behavior—not setup.

Benefits:

  • No need to repeat code
  • Tests are shorter and easier to read
  • Same setup is used everywhere, so tests are more reliable

1. Write Setup Code Once with Fixtures

A fixture is just a function with the @pytest.fixture decorator. You can use the fixture in a test by putting its name as an argument.

Here’s a super simple example:

# test_math.py 

import pytest 

 

@pytest.fixture 

def numbers(): 

    # Provide a reusable set of numbers for testing 

    return (3, 5) 

 

def test_addition(numbers): 

    a, b = numbers 

    assert a + b == 8

What’s happening:

  • The numbers fixture returns the tuple (3, 5).
  • The test_addition function takes it as a parameter.
  • Pytest handles the injection for you — you don’t need to call the fixture manually.

2. Use Fixtures Inside Other Fixtures

Fixtures can also use other fixtures. This helps you build complex setups from small, simple ones.

# test_nested.py 

import pytest 

 

@pytest.fixture 

def base_value(): 

    return 10 

 

@pytest.fixture 

def double_value(base_value): 

    return base_value * 2 

 

def test_double(double_value): 

    assert double_value == 20

How it works:

  1. base_value returns 10.
  2. double_value depends on base_value, multiplying it by 2.
  3. The test gets double_value, which is already resolved to 20.

Execution Flow:

image

Why this is useful:

  • Small fixtures are easier to reuse
  • You only need to change one place if setup changes
  • Each fixture does one simple thing

⚠️ Just be mindful: changing a base fixture will affect all dependent fixtures.

3. Share Fixtures with conftest.py

When you have common setup code used in many test files, the best practice is to define it once in a conftest.py file.

Folder structure:

image (1)

What’s conftest.py?

  • A special Pytest config file.
  • Automatically discovered—no need to import fixtures manually.
  • Fixtures defined here can be used in any test file in the same directory (or subdirectories).

# conftest.py
import pytest

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

  • You’re defining a fixture named user.
  • It returns a simple dictionary with some dummy user data.

# test_user.py
def test_user_role(user):
    assert user["role"] == "admin"

  • This test function accepts user as a parameter.
  • pytest automatically sees the user fixture from conftest.py and injects its return value into the test.
  • The test then asserts that the role of the user is "admin"

Benefits of Using conftest.py

  • Reusability - Write once, use everywhere.
  • No Imports - Pytest finds the fixture, no need to import manually.
  • Clean Code - Test files stay focused on behavior, not setup clutter.
  • Organized - Centralized setup = easier updates and maintenance.

4. Understanding Fixture Scopes: Control How Often Setup Runs

Now that we've seen how powerful fixtures can be for reusability and organization, let’s take it one step further: controlling how often a fixture is run.

This is where fixture scopes come in.

By default, a fixture runs once per test function. But with scope=, you can tell Pytest to reuse the fixture across multiple tests, classes, or even the entire test suite. This is incredibly useful when the setup is expensive or doesn't need to be reset every time.

# test_scope.py
@pytest.fixture(scope="module")
def db():
    print("Connecting to DB")
    return "db_connection"

def test_one(db):
    assert db == "db_connection"

def test_two(db):
    assert db == "db_connection"

Even though two tests use the fixture, the setup (connect_db) runs only once — because it's scoped at the module level.

Output:

image (2)

Why Fixture Scope Matters

Choosing the right scope helps you:

  • Reduce duplicate setup work
  • Speed up test execution
  • Avoid unnecessary teardown/re-initialization

Use narrower scopes (like function) when isolation is key, and broader scopes (like module or session) when setup can safely be shared.

Available Fixture Scopes

Screenshot 2025-09-03 123914

5. Run Tests with Different Values Using Parametrized Fixtures

Parametrized fixtures allow you to run the same test multiple times with different inputs—great for avoiding repetition in your test code.

Here’s a simple example:

# test_params.py 

import pytest 

 

@pytest.fixture(params=[1, 2, 3]) 

def number(request): 

    return request.param 

 

def test_is_even(number): 

    assert (number % 2 == 0) == (number in [2])

What’s happening here?

  • The @pytest.fixture(params=[1, 2, 3]) decorator tells pytest to run the test three times—once for each value in the list.
  • request.param gives the current parameter value to the test.
  • The test checks if the number is even, and confirms that only 2 is considered even in this case.

Why it’s useful

This approach keeps your test code clean and scalable. Instead of writing separate test functions for each input, you define inputs once and reuse the same test logic.

When you run this with pytest -v, you’ll see each test case run separately:

image (3)

A neat, DRY way to test multiple scenarios.

Conclusion

Pytest fixtures are one of the most powerful tools in your testing toolbox. From eliminating redundant setup code to enabling scalable, organized, and efficient tests, they bring structure and clarity to your test suite.