Mocking the filesystem
Use pytest's tmp_path fixture and unittest.mock.patch to test file-writing functions cleanly, without leaving files behind or touching production directories.
- Use pytest's tmp_path fixture to test a file-writing function against a real but temporary directory
- Verify file contents after a write using pathlib
- Use unittest.mock.patch to mock builtins.open when a real path is unavailable
The previous lesson established that you should test your code, not the OS. That
means file-writing functions need a real filesystem for their I/O (to confirm the
bytes are correct) but not the production filesystem. pytest's tmp_path fixture
provides exactly that: a fresh, isolated directory for each test, automatically
removed when the test finishes.
Testing a CSV writer with tmp_path
The demo below defines a function that writes a list of records to a CSV file,
then shows how to test it. The key insight: pass the output path as a parameter
so the test can supply a tmp_path-based location rather than a hardcoded one.
Notice that the test reads the file back and checks individual lines rather than comparing the whole string — this makes failures more informative.
Patching builtins.open
Sometimes you cannot inject the path as a parameter — the function has it
hardcoded or reads it from config. unittest.mock.patch replaces the real open
with a mock for the duration of the test:
from unittest.mock import patch, mock_open
import json
def load_config():
"""Reads config from a hardcoded location — not injectable."""
with open("/etc/myapp/config.json") as f:
return json.load(f)
def test_load_config():
fake_config = json.dumps({"api_key": "test-key", "retries": 3})
with patch("builtins.open", mock_open(read_data=fake_config)):
result = load_config()
assert result["api_key"] == "test-key"
assert result["retries"] == 3mock_open is a factory that produces a mock file object whose .read() returns
read_data. For JSON, that is enough; for CSV-by-line reading you may need to
configure mock_open(read_data=...).return_value.__iter__ manually, at which
point switching to tmp_path is usually cleaner.
Prefer tmp_path over mock_open whenever you can. tmp_path exercises the
actual open/write/read cycle, which catches encoding bugs and newline
issues that a mock never would.
Checking that a function deletes the right file
Deletions are just as testable. Create the file in tmp_path, call the function,
assert the file is gone:
def test_cleanup_removes_temp_files(tmp_path):
temp = tmp_path / "run.tmp"
temp.write_text("leftover")
cleanup(tmp_path) # function under test
assert not temp.exists()Where to go next
Next: mocking HTTP — why you should not hit real APIs in tests, and the
responses library that lets you intercept requests calls at the transport layer.
Why scripts need tests
Automation scripts run unattended, touch real systems, and fail silently — making them the most dangerous category of code to ship without tests.
Mocking HTTP
Real API calls in tests are slow, costly, and non-deterministic. Understand the three strategies for mocking HTTP — and why the responses library wins for most unit tests.