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