<sub>2026-03-13 @1200</sub> #python/pytest #simulator # Two pytest Fixtures That Made Testing Less Painful I have been building a small toolkit that reads live telemetry from a simulator and uses it to train machine learning models. The project is still early. Most of the ML work is ahead of me. But I have been writing unit tests from the start, and two pytest fixtures came up repeatedly enough that I want to write down what I learned about them before I forget. The fixtures are `tmp_path` and `capsys`. Neither is exotic. They are both built into pytest, no plugins required. But I kept reaching for them in situations where I would have otherwise written something messier, and they held up. --- ## `tmp_path`: Testing File-Based Code Without Touching Real Files A chunk of my configuration loading code reads TOML files from disk. It has to handle finding a file that does not exist, reading one that does, and raising an error if an explicitly provided path is wrong. All of that requires real filesystem paths to test properly. My first instinct was to hard-code a path somewhere or use `os.getcwd()` and hope. Neither felt right. `tmp_path` solved this cleanly. Pytest injects `tmp_path` as a function argument. It gives you a `pathlib.Path` pointing to a fresh temporary directory that exists for the duration of that one test, then gets cleaned up automatically. No setup, no teardown, no worry about leftover files from a previous run. In practice I wrote a small helper: ```python def write_toml(path: Path, content: str) -> Path: path.write_text(content, encoding="utf-8") return path ``` And then tests looked like this: ```python def test_reads_nested_toml_sections(self, tmp_path): content = """ [connection] host = "10.0.0.1" rpc_port = 50000 [capture] rate_hz = 10.0 """ p = write_toml(tmp_path / "cfg.toml", content) result = _load_toml(p) assert result["connection"]["host"] == "10.0.0.1" assert result["capture"]["rate_hz"] == 10.0 ``` What I liked about this is that the test is entirely self-contained. The file gets created inside the test, passed into the function under test, and checked. Nothing leaks out. I could run this test a thousand times and never accumulate junk on my filesystem. I also used `tmp_path` in combination with `monkeypatch.chdir()` to test auto-discovery logic. My config loader checks the current working directory for a config file when no explicit path is given. By changing the working directory to an empty `tmp_path` for the duration of the test, I could simulate an environment with no config file present without touching anything real. That felt almost too easy. --- ## `capsys`: Testing Console Output Without Mocking `print()` One of my components prints formatted telemetry rows to the console. The first time a snapshot arrives, it prints a column header. After that, it just prints data rows. I wanted to verify that behavior. My first thought was to mock `print()`. I have done that before and it always feels a little awkward. You end up asserting on call arguments rather than on the actual string that would have appeared on screen. `capsys` is a cleaner approach. Pytest injects it as a fixture, and after any code that calls `print()`, you call `capsys.readouterr()` to get back whatever was written to stdout and stderr as plain strings. Then you just assert on the string content. ```python def test_header_contains_signal_names(self, capsys): sink = ConsoleSink(["altitude", "thrust"]) sink.write(make_snapshot( signal_names=("altitude", "thrust"), values=(500.0, 9800.0), )) captured = capsys.readouterr() assert "altitude" in captured.out assert "thrust" in captured.out ``` I also used it to test a subtler behavior: the header should print exactly once, even if you call `write()` multiple times. I checked that by counting occurrences of a signal name in the full output: ```python assert captured.out.count("altitude") == 1 ``` That is a simple check, but it directly encodes the contract I care about. If someone refactors the header logic and accidentally removes the guard, this test catches it. What felt clean about `capsys` was that I was testing the actual output, not a mock of the mechanism that produces it. The assertions read like English. The test says exactly what I expect to see. --- ## What These Two Fixtures Have in Common Both `tmp_path` and `capsys` reduce friction by handing you something real to work with. Instead of constructing a fake filesystem or patching out `print()`, you get actual objects that behave like the real thing. The tests end up simpler and the failure messages are easier to read. I am still early in this project. The test coverage is thin and there is a lot of ML work ahead that I have not figured out how to test yet. But these two fixtures gave me confidence that the foundation layer is at least honest about what it does. --- ## Notes to Myself 1. Look into whether `tmp_path_factory` is useful when multiple tests in a class need to share a single temporary directory rather than getting fresh ones per test. 2. The `monkeypatch.chdir()` pattern came up more than once alongside `tmp_path`. These two are often paired. Worth understanding the full `monkeypatch` fixture surface area. 3. `capsys` only captures output from Python's built-in `print()`. If I ever switch to a logging-based output system, I will need to look at `caplog` instead. 4. Consider whether testing the exact format of printed output is too brittle. Right now I am checking for substrings like `"000007"` and `"12345.00"`. If the format spec changes, these tests break. That might be a feature, not a bug, but worth keeping in mind. 5. Some of the `TestLoadConfig` tests write empty TOML files just to give the function a valid path. It might be worth exploring whether `monkeypatch` alone could replace some of those, to see if the tests get cleaner.