A Complete Guide to Pytest Fixtures
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:
- The latest version of Python installed
- Basic understanding of using Pytest.
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:
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:
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:
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:
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:
...
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:
...
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
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:
...
@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:
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:
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:
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:
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:
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:
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:
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:
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:
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
:
@pytest.fixture(scope="module")
def library():
...
Now run the tests with:
pytest -v --setup-show
The output will show this:
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:
@pytest.fixture(scope="package")
def library():
...
Now rerun the tests with:
pytest -v --setup-show
The output will show:
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
:
@pytest.fixture(scope="session")
def library():
...
Run the tests with:
pytest -v --setup-show
The output will show:
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:
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:
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:
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:
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!
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 usBuild 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.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github