pytest monkeypatch

Introduction

A test often has to change something for the duration of one test: an environment variable , an attribute of a module, a method of a class. Doing it by hand means writing the old value down and putting it back in a finally block, in every test, correctly.

The monkeypatch fixture of pytest does both parts: the test changes the value through the fixture, and pytest restores the old value at teardown. Nothing is left changed for the next test, even when the test fails.

Tools used here: Python , pytest .

setenv: replace a variable

The fixture is used like any other: the test names it in its signature. monkeypatch.setenv(name, value) sets the variable, and os.environ sees it at once.

In the file settings.py:

import os BASE_URL = os.environ.get("BASE_URL", "http://127.0.0.1:8080") def page_url(path): return BASE_URL.rstrip("/") + path

In the file test_settings.py:

import os import settings def test_environment_value_is_replaced(monkeypatch): monkeypatch.setenv("BASE_URL", "https://example.test") assert os.environ["BASE_URL"] == "https://example.test" def test_environment_is_restored_after_the_test(): assert os.environ.get("BASE_URL") != "https://example.test" def test_module_attribute_is_replaced(monkeypatch): monkeypatch.setattr(settings, "BASE_URL", "http://127.0.0.1:8080") assert settings.page_url("/about/") == "http://127.0.0.1:8080/about/" def test_variable_can_be_removed(monkeypatch): monkeypatch.setenv("PATHS_FILE", "paths.txt") monkeypatch.delenv("PATHS_FILE", raising=False) assert "PATHS_FILE" not in os.environ

python -m pytest test_settings.py -v

============================= test session starts ============================== platform linux -- Python 3.12.16, pytest-9.2.0, pluggy-1.7.0 -- … cachedir: .pytest_cache rootdir: <project> plugins: allure-pytest-2.16.2 collecting ... collected 4 items test_settings.py::test_environment_value_is_replaced PASSED [ 25%] test_settings.py::test_environment_is_restored_after_the_test PASSED [ 50%] test_settings.py::test_module_attribute_is_replaced PASSED [ 75%] test_settings.py::test_variable_can_be_removed PASSED [100%] ============================== 4 passed in 0.04s ===============================

The change is undone automatically

The second test in the file is the proof. It asks for no fixture and only checks that the value the first test set is no longer in the environment. It runs after the first test, and it passes: between the two tests pytest put the environment back exactly as it was. The check is written as != "https://example.test" rather than "BASE_URL" not in os.environ so that it also holds on a machine where BASE_URL is set for real.

The order of teardown matters when the value is read at import time. The fixture is torn down after the test body, and a built-in fixture such as monkeypatch is torn down before an autouse fixture of the same scope, so an autouse fixture that reloads a module reads the original environment. This is what the import-time settings test relies on.

delenv: remove a variable

monkeypatch.delenv(name, raising=False) removes a variable and, like setenv, restores it afterwards. raising=False makes a missing variable a normal case instead of a KeyError. Without it, a test that runs on a machine where the variable is already unset would fail before it checks anything.

The last test of the file used the pair: it sets the variable and removes it again in the same test, then asserts the result.

setattr: replace a module attribute

Not every setting is an environment variable. A module may read a value once and keep it in an attribute. monkeypatch.setattr() replaces that attribute for the test and puts the old object back afterwards:

def test_module_attribute_is_replaced(monkeypatch): monkeypatch.setattr(settings, "BASE_URL", "http://127.0.0.1:8080") assert settings.page_url("/about/") == "http://127.0.0.1:8080/about/"

The same call works on a class or on an instance: monkeypatch.setattr(Client, "timeout", 1), monkeypatch.setattr(client, "send", fake_send). For a function that must not be called at all, the replacement is a small function that records the call or raises.

monkeypatch in the repo

The test container/tests/test_url_selection.py sets PATHS_FILE through the fixture and reloads the module that read it at import:

def _reload(monkeypatch, value): monkeypatch.setenv("PATHS_FILE", value) return importlib.reload(url_selection)

Three lines, and both the successful test and the failing test get the same clean environment after they finish. The full walk-through, with the temporary file the variable points at, is Testing a module that reads a setting at import .

Article author: Arthur Isaev