# 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](https://www.python.org/downloads/) 
- Basic understanding of using
[Pytest](https://betterstack.com/community/guides/testing/pytest-guide/).

## 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:

```command
mkdir pytest-fixtures-demo
```

Next, move into the directory:

```command
cd pytest-fixtures-demo
```

Following that, create a virtual environment to isolate your project
dependencies:

```command
python -m venv venv
```

Now activate the virtual environment:

```command
source venv/bin/activate
```

Upon activation, your terminal will be prefixed with the name of the virtual
environment, which is `venv` in this case:

```command
(venv) <your_username>@<your_computer>:~/pytest-fixtures-demo$
```
Next, create an `app.py` file with the following code:

```python
[label 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:

```command
pip install -U pytest
```

Create a `tests` directory to hold the test files:

```command
mkdir tests
```

For Python to recognize the `tests` directory as a package, create an empty
`__init__.py` file:

```command
touch tests/__init__.py
```

Now, create a file named `test_library.py` inside the
`tests` directory:

```command
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:

```python
[label 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:


```command
pytest -v
```
The output will look like this:

```text
[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:

```python
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:

```python
[label tests/test_library.py]
[highlight]
import pytest
[/highlight]
from app import Library

[highlight]
@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
[/highlight]
...
```
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:

```python
[label tests/test_library.py]
...
def test_add_book():
[highlight]
    library = Library()
[/highlight]
    ...

def test_get_book():
[highlight]
    library = Library()
[/highlight]
    library.add_book("1984", "George Orwell")
    ...


def test_update_book():
[highlight]
    library = Library()
    library.add_book("1984", "George Orwell")
[/highlight]
    ...


def test_list_books():
[highlight]
    library = Library()
    library.add_book("To Kill a Mockingbird", "Harper Lee")
    library.add_book("1984", "George Orwell")
[/highlight]
    ...
```

Then, update the parameters to use the fixtures you defined earlier in the file:


```python
[label tests/test_library.py]
...
[highlight]
def test_add_book(library):
[/highlight]
    library.add_book("The Great Gatsby", "F. Scott Fitzgerald")
    assert library.books == [
        {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald"}
    ]

[highlight]
def test_get_book(library):
[/highlight]
    library.add_book("1984", "George Orwell")
    assert library.get_book(0) == "Title: 1984, Author: George Orwell"

[highlight]
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",
    }
[highlight]

def test_list_books(library_with_books):

    assert library_with_books.list_books() == (
[/highlight]
        "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:


```command
pytest -v
```

```text
[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:


```python
[label tests/test_library.py]  
...

@pytest.fixture
def library():
    return Library()


@pytest.fixture
[highlight]
def library_with_books(library):
[/highlight]
    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:


```command
pytest -v
```

You will see that the tests pass without any issues:


```text
[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:

```text
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:

```command
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:

```python
[label 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:

```command
pytest -v
```

The output will be:

```text
[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:

```command
code tests/conftest.py
```

Replace the contents of the `conftest.py` file with a single fixture:


```python
[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:

```command
tests/test_library.py
```
Clear all the contents and add the following modified tests to use a single fixture:


```python
[label 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:


```command
pytest -v
```

The output should look like this:


```text
[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:


```command
code tests/test_library_operations.py
```

Add the following code to the file:

```python
[label 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:


```command
code tests/test_library_management.py
```

Add the following lines:

```python
[label 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:

```command
rm tests/test_library.py
```

To ensure everything is running well, rerun the Pytest command:

```command
pytest -v 
```

The output should look like this:

```text
[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:

```command
pytest -v --setup-show
```
The output looks like this:

```text
[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`:

```command
code conftest.py
```

And set the scope to `module`:

```python
[label tests/conftest.py]
[highlight]
@pytest.fixture(scope="module")
[/highlight]
def library():
    ...
```

Now run the tests with:

```command
pytest -v --setup-show
```

The output will show this:

```text
[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:

```python
[label tests/conftest.py]
[highlight]
@pytest.fixture(scope="package")
[/highlight]
def library():
    ...
```

Now rerun the tests with:

```command
pytest -v --setup-show
```

The output will show:

```text
[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`:

```python
[label tests/conftest.py]
[highlight]
@pytest.fixture(scope="session")
[/highlight]
def library():
    ...
```

Run the tests with:

```command
pytest -v --setup-show
```

The output will show:

```text
[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:

```command
code tests/test_parametrization.py
```
Add the following code, which uses Pytest's fixture parametrization:

```python
[label 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:

```command
pytest tests/test_parametrization.py -v
```

The output will display output similar to the following:

```text
[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](https://docs.pytest.org/en/latest/reference/reference.html#std-fixture-monkeypatch):
  Temporarily modify functions, classes, dictionaries, etc.
- [request](https://docs.pytest.org/en/latest/reference/reference.html#std-fixture-request):
  Provides information on the test function requesting the fixture.
- [tmpdir](https://docs.pytest.org/en/latest/reference/reference.html#std-fixture-tmpdir):
  Returns a temporary directory path object unique to each test function.
- [tmp_path_factory](https://docs.pytest.org/en/latest/reference/reference.html#tmp-path-factory):
  Returns temporary directories under a common base temp directory.
- [recwarn](https://docs.pytest.org/en/latest/reference/reference.html#std-fixture-recwarn):
  Records warnings emitted by test functions.
- [capsys](https://docs.pytest.org/en/latest/reference/reference.html#pytest.capture.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:

```command
code tests/test_builtin_fixtures.py
```

Then add the following code:

```python
[label 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:

```command
pytest tests/test_builtin_fixtures.py -v --setup-show
```

The output looks like this:

```text
[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:

<!--Note: I didn't see the need to include the filename cause I am just showing how complex the code would look.-->

```text
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:

```command
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](https://docs.pytest.org/en/6.2.x/fixture.html). For
more information on Pytest, see [our guide](https://betterstack.com/community/guides/testing/pytest-guide/).

Thanks for reading, and happy testing!
