A Step-by-Step Guide To Using Pytest Fixtures With Arguments

There’s no doubt that Pytest fixtures are incredibly useful and help you write clean and maintainable tests.

But what if you want to do something more dynamic?

Maybe set up a database connection or pass different data inputs to each test case?

Setting up and tearing down identical fixtures with very minor changes leads to code repetition and maintenance nightmare.

Maybe you want to parameterize your fixtures?

It’s all possible, and the answer lies in Pytest fixtures with arguments.

Fixture utility can be significantly enhanced when they are used with arguments.

This allows for greater flexibility and control in test scenarios, as the same fixture can be used with different data, reducing code duplication and enhancing test readability.

However, understanding how to define and use pytest fixtures with arguments can be complex for some.

This article aims to provide you with clear explanations and practical examples to help leverage Pytest fixtures with arguments effectively.

Let’s get started then.

Refresher on Pytest Fixtures

Before diving in, it would be good to have a quick refresher on Pytest fixtures. Feel free to skip this section if you’re a Pytest veteran.

Pytest fixtures are a powerful feature of the Pytest framework for Python, used for setting up and tearing down test environments.

They provide a fixed baseline upon which tests can reliably and repeatedly execute, enhancing test isolation and reproducibility.

Fixtures can be used to manage resources like database connections, URLs for API testing, or even data sets, making them reusable across multiple tests.

This is an extremely powerful feature of Pytest that allows you to share fixtures across tests and even modules using Pytest Conftest.

Fixtures have a parameter called scope that allows you to control the setup and teardown lifecycle of your fixture.

These are function (default), class, module, session and even package. This article on Pytest fixture scopes is a great read to familiarise yourself with how to use the different scopes.

Having refreshed ourselves on Fixtures, let’s look at some practical needs to include arguments.

Practical Need For Pytest Fixture Arguments

Pytest fixture arguments allow you to pass dynamic values or configurations to fixtures, making your tests more flexible and versatile.

Here are some scenarios that make this feature incredibly useful.

Imagine you have a web application, and you want to test various user roles (e.g., admin, regular user) accessing different parts of the application.

Instead of creating separate fixtures for each role, you can create a single fixture that accepts an argument for the user role and sets up the appropriate permissions and data accordingly.

This way, you reuse the same fixture for multiple test cases, adapting it to different situations.

In another example, you may need to test different database configurations.

For example, your application might support multiple database backends (e.g., SQLite, MySQL, PostgreSQL), and you want to test that your code works correctly with each database.

Using Pytest fixture arguments, you can create a flexible fixture that adapts to different database configurations without duplicating your test code.

We’ll look at how to do this with code examples shortly.

What You’ll Learn

By the end of this article, you should:

  • Have a strong understanding of how to use Pytest fixtures to improve your tests.
  • Use Pytest fixtures with arguments to write reusable fixtures that you can easily share and manipulate within your tests.
  • Perform parameterized testing of Pytest fixtures and the tests they’re used in.

So let’s get into it.

Project Set Up

Prerequisites

To achieve the above objectives, the following is recommended:

  • Basic knowledge of Python (must have)
  • Basics of Pytest and fixtures (nice to have)

Getting Started

Here’s our repo structure

pytest-fixture-example-repo

To get started, clone the repo here, or you can create your own repo by creating a folder and running git init to initialise it.

In this project, we’ll be using Python 3.12.

Create a virtual environment and install the requirements using

1
pip install -r requirements.txt

Pytest Fixtures With Arguments — How To Use

Simple Fixture (Recap)

A simple Pytest fixture can be defined as follows:

tests/test_basic_example.py

1
2
3
4
5
6
7
@pytest.fixture  
def simple_data():
return 42

# Test simple fixture
def test_simple_data(simple_data):
assert simple_data == 42

Straightforward. We define a fixture simple_data and use it within our Unit Test.

1
pytest -v

pytest-fixture-example-simple

Let’s move up a level.

Parameterized Fixture

In our article on Pytest Parameterized Testing, we learned how to use parameters to input values to unit tests.

Now what if we want to do the same for a fixture?

tests/test_basic_example.py

1
2
3
4
5
6
7
8
# Define a fixture with parameters  
@pytest.fixture(params=[0, 1, 2])
def param_data(request):
return request.param

# Test parametrized fixture
def test_param_data(param_data):
assert param_data in [0, 1, 2]

In the above code, we pass the @pytest.fixture(params=[0, 1, 2]) marker to our param_datafixture and made use of the request argument within our fixture.

This helps our fixture consume values from the parameter list - [0, 1, 2].

In the subsequent test, we call that param_data fixture and assert its value.

1
pytest -v

pytest-fixture-example-parameterized-fixture

You can observe that Pytest created 3 instances of our test run for 0, 1, and 2 respectively.

Fixture With Argument (Indirect parametrization)

What if you don’t want to define the parameters in the fixture but rather in the test?

That’s possible too and fairly straightforward.

tests/test_basic_example.py

1
2
3
4
5
6
7
8
9
# Define a fixture that takes an argument  
@pytest.fixture
def square(request):
return request.param * 2

# Use indirect parametrization to pass arguments to the fixture
@pytest.mark.parametrize("square", [1, 2, 3], indirect=True)
def test_square(square):
assert square in [2, 4, 6]

Similar to the previous one, here we have a square function that also takes the request argument and multiplies it by 2.

In our test, we use the indirect=True argument to tell Pytest to pass this parameter to the square fixture.

1
pytest -v

pytest-fixture-example-indirect-parameterization

Factories As Fixtures

Factories, in the context of Pytest fixtures, are functions that are used to create and return instances of objects that are needed for testing.

Factories are a way to generate test data or objects with specific configurations in a reusable manner.

They can be thought of as a type of fixture that specializes in creating instances of classes or generating data.

We can pass arguments to factory functions very easily.

tests/test_basic_example.py

1
2
3
4
5
6
7
8
9
10
11
12
13
@pytest.fixture  
def user_creds():
def _user_creds(name: str, email: str):
return {"name": name, "email": email}

return _user_creds


def test_user_creds(user_creds):
assert user_creds("John", "abc@xyz.com") == {
"name": "John",
"email": "abc@xyz.com",
}

The above code contains a fixture called user_creds that contains a factory function _user_creds() which takes 2 parameters — name and email .

The fixture then returns this factory function.

We can conveniently pass arguments to this fixture — in this case — name=John and email=abc@xyz.com . Very handy indeed.

1
pytest tests/test_basic_example.py -v -s

pytest-fixture-example-factory

More Examples — Pytest Fixture With Argument

Having seen some real examples above, let’s see some more use cases where it may be handy to pass arguments to your fixtures.

Generate Configurable Test Data:

Suppose you have a fixture that generates test data, such as random numbers, strings, or objects. You can pass arguments to the fixture to control the characteristics of the generated data.

1
2
3
4
5
6
7
8
9
@pytest.fixture  
def random_number():
def generate_random_number(min_value, max_value):
return random.randint(min_value, max_value)
return generate_random_number

def test_random_numbers(random_number):
num = random_number(min_value=1, max_value=10)
assert 1 <= num <= 10

Use Different Database Connections or API URLs:

You might have test cases that require different configurations for a resource or object.

By passing arguments to a fixture, you can create instances of the resource with specific settings for each test case.

1
2
3
4
5
6
7
8
9
10
11
@pytest.fixture  
def database_connection():
def connect_to_database(database_name):
# Connect to the specified database
return DatabaseConnection(database_name)
return connect_to_database

def test_read_data_from_database(database_connection):
conn = database_connection(database_name="test_db")
# Test reading data from the "test_db" database
# Additional assertions and test logic here

Where database_name or even the entire config could be passed as an argument or even connect to multiple databases within the same test.

Simulating User Actions:

For UI testing or user interaction testing, you may want to pass arguments to fixtures to simulate different user actions or scenarios.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@pytest.fixture  
def user_actions():
def perform_user_action(action_type):
if action_type == "login":
# Simulate user login
elif action_type == "logout":
# Simulate user logout
else:
raise ValueError("Invalid action type")
return perform_user_action

def test_user_login(user_actions):
user_actions("login")
# Test user login behavior
# Additional assertions and test logic here

Dynamic Resource Allocation:

Fixtures can be used to allocate and release resources like temporary files, network connections, or test environments.

1
2
3
4
5
6
7
8
9
10
11
12
@pytest.fixture  
def temporary_file(request):
def create_temp_file(file_extension):
temp_file = tempfile.NamedTemporaryFile(suffix=file_extension)
request.addfinalizer(temp_file.close)
return temp_file.name
return create_temp_file

def test_file_operations(temporary_file):
file_path = temporary_file(file_extension=".txt")
# Test file operations on the temporary .txt file
# Additional assertions and test logic here

The possibilities are endless and using Pytest fixtures with arguments can open up your testing world.

Conclusion

Although short, I hope this article has been insightful.

Pytest fixtures are without a doubt one of the most powerful features of this great testing framework.

In this article, we covered the practical need for Pytest fixture with arguments.

Next, you learned how to use fixtures with arguments — fixtures with parameters, indirect parameterization and factories as fixtures.

Lastly, we explored some other use cases and the need for Pytest fixtures with arguments.

With this knowledge, you can craft Pytest fixtures with the scope and arguments you need, optimising test quality and time.

If you have ideas for improvement or like me to cover anything specific, please send me a message via Twitter, GitHub or Email.

Till the next time… Cheers!

Additional Reading

Pytest Fixtures
How Pytest Fixtures Can Help You Write More Readable And Efficient Tests
Pytest Conftest With Best Practices And Real Examples
What Are Pytest Fixture Scopes? (How To Choose The Best Scope For Your Test)
How to Effortlessly Generate Unit Test Cases with Pytest Parameterized Tests