Back to Testing guides

A Complete Guide to Pytest Fixtures

Stanley Ulili
Updated on June 21, 2024

Unit testing is essential for software quality, but repetitive setup and data duplication can turn it into a chore. Pytest fixtures offer an elegant solution to this common problem. By encapsulating and reusing common test setups, fixtures significantly enhance your testing workflow.

With Pytest fixtures, you can:

  • Eliminate redundant code and say goodbye to copy-pasting test setup code.
  • Simplify test maintenance by updating test data in a central location.
  • Improve code readability by making your tests more concise and easier to understand.
  • Isolate test environments to ensure each test runs independently and consistently.

In this tutorial, I'll guide you through the ins and outs of Pytest fixtures. You'll learn how to create fixtures, parameterize them for flexibility, and leverage their power to create efficient, reliable, and maintainable unit tests.

Let's get started!

Prerequisites

To follow this tutorial, make sure you have:

Step 1 — Setting up the project directory

In this section, you'll create a project directory and set up a virtual environment to ensure your project dependencies are isolated and managed effectively.

Start by creating a directory that will contain the code you will write in this tutorial:

 
mkdir pytest-fixtures-demo

Next, move into the directory:

 
cd pytest-fixtures-demo

Following that, create a virtual environment to isolate your project dependencies:

 
python -m venv venv

Now activate the virtual environment:

 
source venv/bin/activate

Upon activation, your terminal will be prefixed with the name of the virtual environment, which is venv in this case:

 
(venv) <your_username>@<your_computer>:~/pytest-fixtures-demo$

Next, create an app.py file with the following code:

app.py
class Library:
    def __init__(self):
        self.books = []

    def add_book(self, title, author):
        self.books.append({"title": title, "author": author})
        return "Book added successfully"

    def get_book(self, index):
        if 0 <= index < len(self.books):
            book = self.books[index]
            return f"Title: {book['title']}, Author: {book['author']}"
        else:
            return "Index out of range"

    def update_book(self, index, title, author):
        if 0 <= index < len(self.books):
            self.books[index]["title"] = title
            self.books[index]["author"] = author
            return "Book updated successfully"
        else:
            return "Index out of range"

    def list_books(self):
        if not self.books:
            return "No books in the library"
        return "\n".join(
            f"Title: {book['title']}, Author: {book['author']}" for book in self.books
        )

    def clear_books(self):
        self.books = []

# Example usage:
if __name__ == "__main__":
    library = Library()
    print(library.add_book("1984", "George Orwell"))
    print(library.add_book("To Kill a Mockingbird", "Harper Lee"))
    print(library.update_book(0, "Nineteen Eighty-Four", "George Orwell"))
    print(library.list_books())

The Library class manages a collection of books. It starts with an empty list and has methods to add, retrieve, update, and list books. The add_book() method adds a book with its title and author. The get_book() method retrieves a book by index. The update_book() method updates a book's details based on its index. The list_books() method lists all books in the library, and the clear_books() method clears the list.

To write tests for this class, first install Pytest:

 
pip install -U pytest

Create a tests directory to hold the test files:

 
mkdir tests

For Python to recognize the tests directory as a package, create an empty __init__.py file:

 
touch tests/__init__.py

Now, create a file named test_library.py inside the tests directory:

 
code tests/test_library.py

The code command opens VSCode. If it doesn't, manually open your text editor and create the file.

Add the following code to test the program:

tests/test_library.py
from app import Library  


def test_add_book():
    library = Library()

    library.add_book("The Great Gatsby", "F. Scott Fitzgerald")
    assert library.books == [
        {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald"}
    ]


def test_get_book():
    library = Library()
    library.add_book("1984", "George Orwell")
    assert library.get_book(0) == "Title: 1984, Author: George Orwell"


def test_update_book():
    library = Library()
    library.add_book("1984", "George Orwell")
    library.update_book(0, "The Catcher in the Rye", "J.D. Salinger")

    assert library.books[0] == {
        "title": "The Catcher in the Rye",
        "author": "J.D. Salinger",
    }



def test_list_books():
    library = Library()
    library.add_book("To Kill a Mockingbird", "Harper Lee")
    library.add_book("1984", "George Orwell")
    assert library.list_books() == (
        "Title: To Kill a Mockingbird, Author: Harper Lee\n"
        "Title: 1984, Author: George Orwell"
    )

The test suite starts with test_add_book, which verifies the add_book method. It creates a Library instance, adds a book, and checks if the book is successfully added with the correct details.

The test_get_book function tests the get_book method by adding a book and retrieving it by its index. It ensures the retrieved details are correct and that an out-of-range index returns an error message.

In test_update_book, the update_book method is evaluated. After adding a book, it updates the book's details and confirms the update. It also checks that updating a book at an invalid index returns an error message.

Finally, test_list_books checks the list_books method. It verifies that an empty library returns a message indicating no books, then adds books and confirms the method lists all books accurately.

Run the tests with the following command:

 
pytest -v

The output will look like this:

Output
collected 4 items

tests/test_library.py::test_add_book PASSED                             [ 25%]
tests/test_library.py::test_get_book PASSED                             [ 50%]
tests/test_library.py::test_update_book PASSED                          [ 75%]
tests/test_library.py::test_list_books PASSED                           [100%]

============================== 4 passed in 0.02s ===============================

While the tests indicate that the application is functioning well, the tests themselves have issues. The main issue is the duplication in the instantiation of the Library class. As we add more tests, this duplication will only increase. If the Library class requires new parameters for instantiation, all the tests will need to be updated to include the new parameters.

To avoid this duplication, you will use fixtures.

Step 2 — Getting started with Pytest fixtures

To understand how to use Pytest fixtures, let's look at the four-step process for writing test cases:

  • Arrange: Prepare the test environment by setting up objects, services, database records, URLs, credentials, or any conditions needed for the test.
  • Act: Perform the action you want to test, like calling a function.
  • Assert: Verify that the outcome of the action matches your expectations.
  • Cleanup: Restore the environment to avoid affecting other tests.

Pytest fixtures come into play during the Arrange step. These fixtures are functions that return data in the form of strings, numbers, dictionaries, or objects, which the test functions or methods can use. You can tell Pytest that a function is a fixture with the @pytest.fixture decorator.

A fixture can be as simple as this:

 
import pytest

@pytest.fixture
def numbers():
    return [1, 2, 3]


def test_sum_numbers(numbers):
    assert sum(numbers) == 6

The numbers fixture returns a list [1, 2, 3], which is used in the test_sum_numbers function to verify the sum of the list.

You can apply this same concept to the program by defining two fixtures. Create fixtures that return an instance of an empty Library() and one with books:

tests/test_library.py
import pytest
from app import Library
@pytest.fixture
def library():
return Library()
@pytest.fixture
def library_with_books():
library = Library()
library.add_book("To Kill a Mockingbird", "Harper Lee")
library.add_book("1984", "George Orwell")
return library
...

Here you add two Pytest fixtures. The first fixture, library, returns a new instance of the Library class, while the second fixture, library_with_books, returns an instance of Library with two books already added.

Next, remove the instantiation in each test:

tests/test_library.py
...
def test_add_book():
library = Library()
... def test_get_book():
library = Library()
library.add_book("1984", "George Orwell") ... def test_update_book():
library = Library()
library.add_book("1984", "George Orwell")
... def test_list_books():
library = Library()
library.add_book("To Kill a Mockingbird", "Harper Lee")
library.add_book("1984", "George Orwell")
...

Then, update the parameters to use the fixtures you defined earlier in the file:

tests/test_library.py
...
def test_add_book(library):
library.add_book("The Great Gatsby", "F. Scott Fitzgerald") assert library.books == [ {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald"} ]
def test_get_book(library):
library.add_book("1984", "George Orwell") assert library.get_book(0) == "Title: 1984, Author: George Orwell"
def test_update_book(library_with_books):
library_with_books.update_book(0, "The Catcher in the Rye", "J.D. Salinger")
assert library_with_books.books[0] == {
"title": "The Catcher in the Rye",
"author": "J.D. Salinger",
}
def test_list_books(library_with_books):
assert library_with_books.list_books() == (
"Title: To Kill a Mockingbird, Author: Harper Lee\n" "Title: 1984, Author: George Orwell" )

Each test function now uses these fixtures to obtain the necessary Library instance.

The benefits of using fixtures include improved code readability and maintainability, as the setup logic is centralized and reused across multiple tests. Additionally, fixtures provide a clear separation of test setup and execution, making the tests easier to understand and modify. This approach also allows for more flexible and modular testing, as different initial states can be easily configured and tested using different fixtures.

Save the new changes and run the tests:

 
pytest -v
Output
collected 4 items

tests/test_library.py::test_add_book PASSED                             [ 25%]
tests/test_library.py::test_get_book PASSED                             [ 50%]
tests/test_library.py::test_update_book PASSED                          [ 75%]
tests/test_library.py::test_list_books PASSED                           [100%]

============================== 4 passed in 0.02s ===============================

With that, the tests are passing. Now that you know how fixtures work and the benefits they bring, you can move on to the next section to request fixtures from other fixtures.

Step 3 — Requesting fixtures from other fixtures

Fixtures in Pytest are modular, allowing one fixture to use another.

For example, you can rewrite the library_with_books fixture to call the library() fixture like this:

tests/test_library.py
...

@pytest.fixture
def library():
    return Library()


@pytest.fixture
def library_with_books(library):
library.add_book("To Kill a Mockingbird", "Harper Lee") library.add_book("1984", "George Orwell") return library

In the highlighted code block, the library_with_books fixture depends on the library fixture to create a Library instance with two pre-added books. This setup enhances reusability and modularity by allowing the library fixture to be reused across multiple tests and fixtures, reducing redundancy.

Now run the tests with the following command:

 
pytest -v

You will see that the tests pass without any issues:

Output
collected 4 items

tests/test_library.py::test_add_book PASSED                                                                                                                                                                [ 25%]
tests/test_library.py::test_get_book PASSED                                                                                                                                                                [ 50%]
tests/test_library.py::test_update_book PASSED                                                                                                                                                             [ 75%]
tests/test_library.py::test_list_books PASSED                                                                                                                                                              [100%]

================================================================================================ 4 passed in 0.02s ================================================================================================

Using fixtures within other fixtures is a useful feature. However, it's important to be cautious about fixture dependencies, ensuring that changes in the base fixture (library) do not unintentionally affect the dependent fixture (library_with_books). Proper state management is crucial to avoid test flakiness, ensuring each test has a clean and independent state to maintain reliable and predictable test outcomes.

Step 4 — Using fixtures across multiple files with the conftest.py file

As your project grows, it's common to have multiple test files that use the same fixtures. Setting up fixtures in every test file is not efficient. A good solution is to create a conftest.py file and add the fixtures there. Pytest will automatically discover these fixtures and make them available to all test files without needing to import them.

To do that, you define the fixtures inside the conftest.py file in the tests directory like this:

 
tests/
├── conftest.py
├── test_example_one.py
├── subdirectory/
│   ├── conftest.py
│   └── test_example_two.py
└── new_subdirectory/
    ├── conftest.py
    └── test_example_three.py

Once the fixtures are defined in conftest.py, all test files, including those in subdirectories, will be able to use the fixtures.

First, create the conftest.py file:

 
code tests/conftest.py

Next, open the tests/test_library.py and move all the imports and fixtures to the conftest.py file like this:

tests/conftest.py
import pytest
from app import Library


@pytest.fixture
def library():
    return Library()

@pytest.fixture
def library_with_books(library):
    library.add_book("To Kill a Mockingbird", "Harper Lee")
    library.add_book("1984", "George Orwell")
    yield library
    # Teardown code
    library.clear_books()

With that change, the tests/test_library.py will only contains the test functions.

Once you save the changes, run the tests again. They will run without any issues, even though the test files don't have any fixture imports:

 
pytest -v

The output will be:

Output
collected 4 items

tests/test_library.py::test_add_book PASSED                                                                                                                                                                [ 25%]
tests/test_library.py::test_get_book PASSED                                                                                                                                                                [ 50%]
tests/test_library.py::test_update_book PASSED                                                                                                                                                             [ 75%]
tests/test_library.py::test_list_books PASSED                                                                                                                                                              [100%]

================================================================================================ 4 passed in 0.02s ================================================================================================

When you use the conftest.py, you ensure that all test files can access the same fixtures without redundant imports.

Step 5 — Understanding Pytest fixture scopes

In Pytest, fixtures are a powerful way to set up the resources needed for tests. One key feature of fixtures is their scope, which determines how long a fixture will be active and when it will be set up and torn down.

The following are the scopes ordered from the lowest to the highest scope:

  • Function Scope: This is the default scope. The fixture is set up before each test function and torn down after the test function finishes.
  • Class Scope: The fixture is set up once per test class and is available to all test methods within that class.
  • Module Scope: The fixture is set up once per test module and is available to all test functions within that module.
  • Package Scope: The fixture is set up once per package and is available to all tests within that package.
  • Session Scope: The fixture is set up once per entire test session and is available to all tests that run during that session.

First, open the conftest.py file:

 
code tests/conftest.py

Replace the contents of the conftest.py file with a single fixture:

 
[label ]
import pytest
from app import Library


@pytest.fixture
def library():
    lib = Library()
    lib.add_book("To Kill a Mockingbird", "Harper Lee")
    lib.add_book("1984", "George Orwell")
    yield lib
    # Teardown code
    lib.clear_books()

Next, open the test file:

 
tests/test_library.py

Clear all the contents and add the following modified tests to use a single fixture:

tests/test_library.py
def test_add_book(library):
    library.add_book("The Great Gatsby", "F. Scott Fitzgerald")
    expected_books = [
        {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
        {"title": "To Kill a Mockingbird", "author": "Harper Lee"},
        {"title": "1984", "author": "George Orwell"},
    ]
    assert sorted(library.books, key=lambda x: x["title"]) == sorted(
        expected_books, key=lambda x: x["title"]
    )


def test_get_book(library):
    library.add_book("1984", "George Orwell")
    assert library.get_book(2) == "Title: 1984, Author: George Orwell"


def test_update_book(library):
    library.update_book(0, "The Catcher in the Rye", "J.D. Salinger")

    assert library.books[0] == {
        "title": "The Catcher in the Rye",
        "author": "J.D. Salinger",
    }


def test_list_books(library):
    assert library.list_books() == (
        "Title: To Kill a Mockingbird, Author: Harper Lee\n"
        "Title: 1984, Author: George Orwell"
    )

The major change here is that the test_add_book uses the library fixture. To ensure the books are added correctly, the test checks for the presence of both existing and newly added books.

To make sure the tests work well, run the following command:

 
pytest -v

The output should look like this:

Output
collected 4 items

tests/test_library.py::test_add_book PASSED                                                                                                                                                                [ 25%]
tests/test_library.py::test_get_book PASSED                                                                                                                                                                [ 50%]
tests/test_library.py::test_update_book PASSED                                                                                                                                                             [ 75%]
tests/test_library.py::test_list_books PASSED                                                                                                                                                              [100%]

================================================================================================ 4 passed in 0.02s ================================================================================================

However, we don't want all the tests in a single file as it would be difficult to understand the difference between the scopes, especially when it comes to module, package, or session scopes. So, we will split the test file into two files.

First, create a test file that will only have two methods for testing if books can be added and retrieved:

 
code tests/test_library_operations.py

Add the following code to the file:

test_library_operations.py
def test_add_book(library):
    library.add_book("The Great Gatsby", "F. Scott Fitzgerald")
    expected_books = [
        {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
        {"title": "To Kill a Mockingbird", "author": "Harper Lee"},
        {"title": "1984", "author": "George Orwell"},
    ]
    assert sorted(library.books, key=lambda x: x["title"]) == sorted(
        expected_books, key=lambda x: x["title"]
    )


def test_get_book(library):
    library.add_book("1984", "George Orwell")
    assert library.get_book(2) == "Title: 1984, Author: George Orwell"

Next, create the second test file that will test updating the book and listing all books:

 
code tests/test_library_management.py

Add the following lines:

test_library_management.py
def test_update_book(library):
    library.update_book(0, "The Catcher in the Rye", "J.D. Salinger")

    assert library.books[0] == {
        "title": "The Catcher in the Rye",
        "author": "J.D. Salinger",
    }


def test_list_books(library):
    assert library.list_books() == (
        "Title: To Kill a Mockingbird, Author: Harper Lee\n"
        "Title: 1984, Author: George Orwell"
    )

Remove the test_library.py file:

 
rm tests/test_library.py

To ensure everything is running well, rerun the Pytest command:

 
pytest -v 

The output should look like this:

Output
tests/test_library_management.py ..                                                                                                                                                                         [ 50%]
tests/test_library_operations.py ..                                                                                                                                                                         [100%]

================================================================================================ 4 passed in 0.02s ================================================================================================

With the tests passing, you can now explore the function scope.

Function scope

The function scope is the default scope. This means the fixture is set up for each test function and destroyed after each test. So, any example you have seen used the function scope. This ensures that each test has a clean state.

Run the tests again with the --setup-show flag to observe the setup and teardown process:

 
pytest -v --setup-show

The output looks like this:

Output
collected 4 items

tests/test_library_management.py::test_update_book
        SETUP    F library
        tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
        TEARDOWN F library
tests/test_library_management.py::test_list_books
        SETUP    F library
        tests/test_library_management.py::test_list_books (fixtures used: library)PASSED
        TEARDOWN F library
tests/test_library_operations.py::test_add_book
        SETUP    F library
        tests/test_library_operations.py::test_add_book (fixtures used: library)PASSED
        TEARDOWN F library
tests/test_library_operations.py::test_get_book
        SETUP    F library
        tests/test_library_operations.py::test_get_book (fixtures used: library)PASSED
        TEARDOWN F library

================================================================================================ 4 passed in 0.02s ================================================================================================

The SETUP F library and TEARDOWN F library lines indicate that the fixture is set up before each test and torn down after each test, ensuring a fresh state for each test. Here, F stands for function scope, which is the default scope in Pytest. This means that the fixture is applied separately for each test function.

This approach ensures isolation between tests, preventing side effects from shared state. However, it can be expensive if you are setting up resources like a database connection, as there is no real reason to keep creating a fresh connection for each test.

Module scope

A module in Python is a single file that can be imported using the import keyword. Pytest allows you to define a module scope for your fixtures. The module scope allows fixtures to be shared by all tests within a module, ensuring a consistent state. This means that the fixture is instantiated once and persists throughout the execution of the test code in the module. When the last test executes, the fixture is then destroyed.

This scope is often good for scenarios where:

  • The setup or teardown process involves resource-intensive operations like database connections or loading configuration specific to a module.
  • Performance is improved because it reduces repeated setups and teardowns, as the instance is reused across all the tests that require the shared fixture.

However, be cautious that if your tests modify the fixture, it can lead to inconsistencies across the tests. Therefore, you should use this only when you are certain that the state remains unmodified, or if modified, the other tests can handle it.

To understand how the module scope works with multiple files, open the conftest.py:

 
code conftest.py

And set the scope to module:

tests/conftest.py
@pytest.fixture(scope="module")
def library(): ...

Now run the tests with:

 
pytest -v --setup-show

The output will show this:

Output
tests/test_library_management.py::test_update_book
    SETUP    M library
        tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
tests/test_library_management.py::test_list_books
        tests/test_library_management.py::test_list_books (fixtures used: library)FAILED
    TEARDOWN M library
tests/test_library_operations.py::test_add_book
    SETUP    M library
        tests/test_library_operations.py::test_add_book (fixtures used: library)PASSED
tests/test_library_operations.py::test_get_book
        tests/test_library_operations.py::test_get_book (fixtures used: library)FAILED
    TEARDOWN M library

...
============================================================================================= short test summary info =============================================================================================
FAILED tests/test_library_management.py::test_list_books - AssertionError: assert 'Title: The C...George Orwell' == 'Title: To Ki...George Orwell'
FAILED tests/test_library_operations.py::test_get_book - AssertionError: assert 'Title: The G...tt Fitzgerald' == 'Title: 1984,...George Orwell'
=========================================================================================== 2 failed, 2 passed in 0.04s ===========================================================================================

In the output, you can see that for all tests in each module (each test file), the setup and teardown are wrapped with SETUP M library and TEARDOWN M library. This means the fixture is set up once before any tests in the module run and torn down after all tests in the module complete. This reduces the overhead of creating and tearing down the library for each test, providing a more efficient setup while still ensuring isolation between different test files.

However, because the fixture is shared within the module, changes made to the fixture state in one test can affect subsequent tests. This is why some tests may fail, as they assume the fixture starts with a clean state but it does not.

Package scope

The package scope is useful for sharing fixtures across multiple modules within the same package. A package scope fixture is created at the beginning of the package (a directory containing an __init__.py file) and is shared across all subdirectories (packages) within that package. The fixture is then destroyed during the teardown of the last test in the package.

To apply the package scope, update the conftest.py file:

tests/conftest.py
@pytest.fixture(scope="package")
def library(): ...

Now rerun the tests with:

 
pytest -v --setup-show

The output will show:

Output
collected 4 items

tests/test_library_management.py::test_update_book
  SETUP    P library
        tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
tests/test_library_management.py::test_list_books
        tests/test_library_management.py::test_list_books (fixtures used: library)FAILED
tests/test_library_operations.py::test_add_book
        tests/test_library_operations.py::test_add_book (fixtures used: library)FAILED
tests/test_library_operations.py::test_get_book
        tests/test_library_operations.py::test_get_book (fixtures used: library)FAILED
  TEARDOWN P library

In this output, you can see that the fixture is set up once at the beginning of the package (SETUP P library) and torn down at the end (TEARDOWN P library). As a result, once the test_update_book fixture makes an update, all the other tests fail because they are written in a way that they expect the fixture to be a clean slate, which isn't the case.

Just like with the module scope, it is important when using the package scope to ensure that the tests are not modifying the fixture; otherwise, subsequent tests may fail due to state changes.

Session scope

The session scope is another scope that can be helpful when you want functions to share the same test setup. A session scope fixture is created at the beginning of the test run and is destroyed after all the tests have finished. They persist for the entire test session, making them useful for fixtures that set up expensive resources like database connections for the entire test session.

Set the scope to session:

tests/conftest.py
@pytest.fixture(scope="session")
def library(): ...

Run the tests with:

 
pytest -v --setup-show

The output will show:

Output
collected 4 items

tests/test_library_management.py::test_update_book
SETUP    S library
        tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
tests/test_library_management.py::test_list_books
        tests/test_library_management.py::test_list_books (fixtures used: library)FAILED
tests/test_library_operations.py::test_add_book
        tests/test_library_operations.py::test_add_book (fixtures used: library)FAILED
tests/test_library_operations.py::test_get_book
        tests/test_library_operations.py::test_get_book (fixtures used: library)FAILED
TEARDOWN S library
...

=========================================================================================== 3 failed, 1 passed in 0.04s ===========================================================================================

In session scope, the library fixture is set up once at the beginning of the entire test session and is torn down after all tests have been completed. This scope is the most efficient for minimizing setup and teardown operations, as it performs these actions only once for the entire test session, regardless of the number of test packages or modules involved

Another scope that we didn't explore in detail is the class scope. This scope is useful when you want a fixture to be set up once for all the methods in a test class, rather than being torn down after each test function. This allows the fixture to be reused by all methods in the class, making it efficient, especially if the tests are isolated or if the setup involves something expensive that should be set up and torn down infrequently. You can review the documentation for more details.

Step 6 — Parametrizing fixtures

In Python, functions using fixtures can be parametrized to write concise and readable tests. Test functions using these fixtures are called multiple times, executing with a different argument each time. This approach is beneficial when dealing with various database connection values, multiple files, etc.

To use parametrization, you pass the params keyword argument to the fixture decorator with a set of input values, like this: @pytest.fixture(params=[values...]).

To see how it works, create a test_parametrization.py file:

 
code tests/test_parametrization.py

Add the following code, which uses Pytest's fixture parametrization:

tests/test_parametrization.py
import pytest

@pytest.fixture(params=["image_1.jpg", "document_1.pdf", "image_2.png", "image_3.jpeg"])
def original_file_path(request):
    return request.param

def convert_to_hyphens(file_path):
    return file_path.replace("_", "-")

def test_convert_to_hyphens(original_file_path):
    converted_file_path = convert_to_hyphens(original_file_path)
    assert "-" in converted_file_path

The original_file_path() fixture supplies various file paths from the params list. The test_convert_to_hyphens() test depends on this fixture and runs multiple times, once for each file path in the list. This ensures comprehensive testing of the convert_to_hyphens() function, which replaces underscores with hyphens.

To execute the tests, run:

 
pytest tests/test_parametrization.py -v

The output will display output similar to the following:

Output
collected 4 items

tests/test_parametrization.py::test_convert_to_hyphens[image_1.jpg] PASSED                                                                                                                                  [ 25%]
tests/test_parametrization.py::test_convert_to_hyphens[document_1.pdf] PASSED                                                                                                                               [ 50%]
tests/test_parametrization.py::test_convert_to_hyphens[image_2.png] PASSED                                                                                                                                  [ 75%]
tests/test_parametrization.py::test_convert_to_hyphens[image_3.jpeg] PASSED                                                                                                                                 [100%]

================================================================================================ 4 passed in 0.02s ================================================================================================

This output shows that the test function ran four times, each with a different input.

With this, you can now effectively parametrize fixtures.

Step 7 — Using built-in fixtures

Pytest comes with built-in fixtures that cover common testing scenarios. These fixtures simplify writing and maintaining tests by reducing boilerplate code and ensuring consistency through standard features.

Some of the built-in fixtures include:

  • monkeypatch: Temporarily modify functions, classes, dictionaries, etc.
  • request: Provides information on the test function requesting the fixture.
  • tmpdir: Returns a temporary directory path object unique to each test function.
  • tmppathfactory: Returns temporary directories under a common base temp directory.
  • recwarn: Records warnings emitted by test functions.
  • capsys: Captures writes to sys.stdout and sys.stderr.

In this section, let's look at the tmp_path fixture. This fixture is essential for managing temporary directories, allowing you to avoid working with real directories, which can be complex due to different file paths on various platforms (Windows, macOS, Unix). The tmp_path fixture provides a controlled, isolated, and platform-agnostic approach to managing directories, with automatic setup and teardown.

To test this, create a test_builtin_fixtures.py file:

 
code tests/test_builtin_fixtures.py

Then add the following code:

tests/test_builtin_fixtures.py
def test_create_and_verify_temp_file(tmp_path):
    # Create a temporary directory within tmp_path
    temporary_directory = tmp_path / "example_temp_dir"
    temporary_directory.mkdir()

    # Create a file inside the temporary directory
    temporary_file = temporary_directory / "example_file.txt"
    temporary_file.write_text("Temporary file content")

    # Verify that the file exists
    assert temporary_file.is_file()

    # Verify the file's contents
    assert temporary_file.read_text() == "Temporary file content"

This code creates a temporary directory and file using the tmp_path fixture. It verifies the file's existence and contents, ensuring that the test environment is isolated and consistent across different platforms.

Now you can run the file:

 
pytest tests/test_builtin_fixtures.py -v --setup-show

The output looks like this:

Output
collected 1 item

tests/test_builtin_fixtures.py::test_create_and_verify_temp_file
SETUP    S tmp_path_factory
        SETUP    F tmp_path (fixtures used: tmp_path_factory)
        tests/test_builtin_fixtures.py::test_create_and_verify_temp_file (fixtures used: request, tmp_path, tmp_path_factory)PASSED
        TEARDOWN F tmp_path
TEARDOWN S tmp_path_factory

================================================================================================ 1 passed in 0.01s ===============================================================================================

Without using the fixtures, the test would be as complex as this:

 
import tempfile
import os
import pytest


def create_and_verify_temp_file(temp_dir):
    # Create a file inside the temporary directory
    temp_file_path = os.path.join(temp_dir, "example_file.txt")

    with open(temp_file_path, "w") as temp_file:
        temp_file.write("Temporary file content")

    # Verify that the file exists
    assert os.path.isfile(temp_file_path)

    # Verify the file's contents
    with open(temp_file_path, "r") as temp_file:
        content = temp_file.read()
        assert content == "Temporary file content"


def test_create_and_verify_temp_file_without_fixture():
    # Create a temporary directory
    with tempfile.TemporaryDirectory() as temp_dir:
        # Call the function to create and verify the file
        create_and_verify_temp_file(temp_dir)

This code is not only harder to read but also verbose. Using fixtures simplifies this and makes the test more readable and maintainable.

If the built-in fixtures don't suffice, Pytest has an extensive list of third-party plugins. Some of these plugins provide useful fixtures. Using them involves installing the package:

 
pip install <package_name>

Then, you can use the fixtures provided by these plugins like you use built-in fixtures without any additional imports.

Final thoughts

This article comprehensively covers Pytest fixtures, highlighting their modularity and various scopes.

To learn more about Pytest fixtures, visit the Pytest documentation page. For more information on Pytest, see our guide.

Thanks for reading, and happy testing!

Author's avatar
Article by
Stanley Ulili
Stanley is a freelance web developer and researcher from Malawi. He loves learning new things and writing about them to understand and solidify concepts. He hopes that by sharing his experience, others can learn something from them too!
Got an article suggestion? Let us know
Next article
A Gentle Introduction to Python's unittest Module
This article provides a comprehensive guide to writing, organizing, and executing unit tests in Python using the unittest module
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github