DEV Community

Cover image for Pytest Built-in Fixtures
Eric Chen
Eric Chen

Posted on

Pytest Built-in Fixtures

As I've been onboarding to my new position, I've been reviewing some of my coworker's code. I learned that pytest 1) has some built-in fixtures, and 2) they are really useful!

First, briefly, what are fixtures? When writing pytest tests, it's pretty frequent to write fixtures for reusability. They are used to set up tests. For example, a common fixture is:

@pytest.fixture()
def dataset_path():
    return "/dataset/imagenet/train"

def test_dataloader(dataset_path):
    dataloader = DataLoader(dataset_path)
    assert dataloader is not None
Enter fullscreen mode Exit fullscreen mode

When test_dataloader runs, pytest sees it takes a dataset_path argument, finds the matching fixture, and injects the returned path automatically.

What is really cool is that pytest actually comes with some pretty useful built-in fixtures. Two are tmp_path and monkeypatch.

tmp_path gives a random path (a pathlib.Path object) unique for each test. For example, a minimal example is:

from pathlib import Path

def test_path1(tmp_path: Path):
    print(tmp_path)

def test_path2(tmp_path: Path):
    print(tmp_path)
Enter fullscreen mode Exit fullscreen mode

Terminal:

uv run pytest -s
======================================================== test session starts =========================================================
platform darwin -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/user/Developer/til
configfile: pyproject.toml
collected 2 items

test/random_tst/random_test.py 
/private/var/folders/5t/n5nrkl1n77q5vlvsxys89hhc0000gn/T/pytest-of-user/pytest-4/test_path10
.
/private/var/folders/5t/n5nrkl1n77q5vlvsxys89hhc0000gn/T/pytest-of-user/pytest-4/test_path20
.
Enter fullscreen mode Exit fullscreen mode

While tmp_path is straightforward, monkeypatch is like a Swiss army knife.

From the pytest documentation, it can set and delete attributes of objects, set and delete items in a map, set and delete environment variables, etc.:

monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=None)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
monkeypatch.context()
Enter fullscreen mode Exit fullscreen mode

source

Here is one example to set environment variables:

import os

def test_monkeypatch(monkeypatch):
    print(f"TEST_ENV: {os.getenv('TEST_ENV')}")
    monkeypatch.setenv("TEST_ENV", "test")
    print(f"TEST_ENV: {os.getenv('TEST_ENV')}")

def test_monkeypatch_resets():
    print(f"TEST_ENV: {os.getenv('TEST_ENV')}")
Enter fullscreen mode Exit fullscreen mode

This outputs:

TEST_ENV: None
TEST_ENV: test
TEST_ENV: None
Enter fullscreen mode Exit fullscreen mode

You can see that the environment resets after the test using the monkeypatch fixture completes. This is the case with all monkeypatch usage, which makes it especially useful for tests that need to modify objects, dictionaries, and environment variables as setup.

In contrast, we can also modify the environment using the os package like so:

import os

def test_os_setenv():
    print(f"TEST_ENV: {os.getenv('TEST_ENV')}")
    os.environ["TEST_ENV"] = "test"
    print(f"TEST_ENV: {os.getenv('TEST_ENV')}")

def test_os_setenv_does_not_reset():
    print(f"TEST_ENV: {os.getenv('TEST_ENV')}")
Enter fullscreen mode Exit fullscreen mode

Outputs:

TEST_ENV: test
TEST_ENV: test
Enter fullscreen mode Exit fullscreen mode

Here, both tests have TEST_ENV set even though the second test never sets the environment variable itself. This means the change made by os.environ carries over between tests. monkeypatch is a safer alternative for testing because it resets after each test function completes.

You can find the whole list of pytest built-in fixtures here: https://docs.pytest.org/en/stable/reference/fixtures.html#built-in-fixtures

Moral of the story: when you need to do something in pytest, check to see if there's a pytest-provided alternative first. It can often make things much cleaner and easier to maintain.

Top comments (0)