Below is a classic setup-teardown pattern with pytest fixtures. We're basically doing temporary monkey-patching of a JSON file for testing, then rolling it all back like nothing ever happened.
# test_pytest_fixture_yield.py
import os
import json
import pytest
from pathlib import Path
"""
demo usage of pytest.fixture to use yield to do cleanup/teardown
"""
json_path = Path(os.path.join(os.getcwd()), "pytest", "test.json")
@pytest.fixture
def mock_json_file():
# Backup the original content
with open(json_path, 'r') as f:
original_content = json.load(f)
# Write mock content
mock_content = {
"name": "Mocky McMockface",
"age": 66
}
with open(json_path, 'w') as f:
json.dump(mock_content, f)
# Hand control to the test
yield
# Restore original content
with open(json_path, 'w') as f:
json.dump(original_content, f)
def read_json():
with open(json_path) as f:
data = json.load(f)
return data
@pytest.mark.usefixtures("mock_json_file")
def test_something():
data = read_json()
assert data["name"] == "Mocky McMockface"
-
Why use yield in fixtures? Because it gives us a clean split between setup (before yield) and teardown (after yield).
-
But dont I need to use the pytest fixture in my pytest for it to get invoked? Nope. Pytest runs the fixture function before the test starts.
-
So the fixture function yields, the execution goes to my test function which runs the fixture function - does the json swap first time - then exits the test function. But I thought we need to call the fixture function with
next()to continue or no? Pytest is secretly doing thenext()for you.
Let’s take this example:
@pytest.fixture
def mock_json_file():
print("Setting up: mocking JSON file")
yield
print("Tearing down: restoring original JSON file")
And the test:
def test_something(mock_json_file):
print("Running the test")
When pytest runs this test, it does something like:
gen = mock_json_file() # creates generator
next(gen) # executes code up to the yield
# --> "Setting up" prints
# now it runs your test
# --> "Running the test" prints
try:
next(gen) # runs the code *after* yield (the teardown)
# --> "Tearing down" prints
except StopIteration:
pass
So pytest calls next() automatically for us. Thats hella cool!!! =]
┌────────────┐
│ fixture() │
└────┬───────┘
│
┌─────▼─────┐
│ setup │
│ (before yield)
└─────┬─────┘
│
▼
┌────────────┐
│ yield │ ← test runs here
└─────┬──────┘
│
┌─────▼──────┐
│ teardown │
└────────────┘
"But I thought we need to call the function with yield to continue?”
Yes — normally with your own generator, you need to control it. But with pytest, pytest becomes the controller. It:
- Calls your fixture.
- Handles the generator lifecycle.
- Makes sure setup and teardown happen automatically, and always — even if the test crashes.
With regards to @pytest.mark.usefixtures("<fixture_name>") see this from the official docs.
They say
Due to the usefixtures marker, the cleandir fixture will be required for the execution of each test method, just as if you specified a “cleandir” function argument to each of them.
So basically if we have a pytest fixture and we dont actually need the return value from the fixture in our pytest function (but just need the code inside the pytest fixture to run), we dont need to pass the pytest fixture as argument to our pytest function. Instead decorate our pytest function with @pytest.mark.usefixtures("<fixture_name>")
More advanced : We can copy the json file to a temp path (tmp_path) and patch our app to read from there during testing.
import os
import json
import pytest
from pathlib import Path
"""
same as test_pytest_fixture_yield.py but uses tmp_path builtin fixture in our fixture function
"""
@pytest.fixture
def mock_json_file(tmp_path):
# Create a mock file path inside the temp directory
temp_json_path = tmp_path / "test.json"
# Write mock content to the temp file
mock_content = {
"name": "Mocky McMockface",
"age": 66
}
with open(temp_json_path, 'w') as f:
json.dump(mock_content, f)
yield temp_json_path # yield the path to the test
def read_json(file_path):
with open(file_path) as f:
data = json.load(f)
return data
def test_something(mock_json_file):
data = read_json(mock_json_file)
assert data["name"] == "Mocky McMockface"