How do you assert what your code wrote through a mock_open file handle?
answer
- Two levels: the open call, the handle
- Grab the handle without calling it
- Rebuild the document, do not count chunks
- writelines lands on its own child
- mock_calls holds the ordered script
basics
~10 sReach the handle through the mock's return_value, then assert on its write child: assert_called_once_with for a single write, or join the first argument of every call in handle.write.call_args_list to rebuild the whole document.
solid answer
~40 sThe double you patched over `open` records the call to `open` itself; the writes land on its `return_value`, the handle. So `m.assert_called_once_with(path, "w")` checks the open, and `m.return_value.write` carries the writes. Take the handle as `m.return_value`, **not** as `m()` — calling the mock to fetch the handle records another call to `open` and breaks any `assert_called_once_with` on it. For a document written in pieces, rebuild it with `"".join(c.args[0] for c in handle.write.call_args_list)` and assert on the string; asserting on individual `write` calls couples the test to the code's chunking. `writelines` is a separate child and does not appear in `write.call_args_list`. For ordering across the whole session, `m.mock_calls` holds the interleaved script including `__enter__`, `write`, `__exit__` and `close`.
code
python · 12 linesfrom unittest.mock import mock_open, patch
m = mock_open()
with patch("builtins.open", m):
with open("/var/log/ingest.offsets", "w") as f:
f.write("shard=0 offset=41\n")
f.write("shard=1 offset=17\n")
m.assert_called_once_with("/var/log/ingest.offsets", "w")
handle = m.return_value # m() would record another call
written = "".join(c.args[0] for c in handle.write.call_args_list)
assert written == "shard=0 offset=41\nshard=1 offset=17\n"go deeper
Remember that writes are recorded on the handle, not on the double you patched over open. Know how to reach it as m.return_value and how to look at write.call_args_list when an assertion surprises you.
Explain the two-level record and why m() to fetch the handle corrupts the call count. Be able to rebuild the written document from call_args_list rather than asserting on the code's chunking.
Show judgement about what the assertion proves. Point out writelines, the shared handle across two files, and the fact that a green close() assertion says nothing about durability or encoding.
Frame where this style of assertion belongs in the suite at all. Argue when verifying the calls a component made is the right contract to pin, and when the team should be asserting on real output instead.
When you patch `open` with `unittest.mock.mock_open`, you end up with a two-level record, and almost every mistake in this area comes from asserting at the wrong level. ## The two levels The mock you installed — call it `m` — stands in for `open`. Its own call record answers *was the file opened, with what path and mode*: `m.assert_called_once_with("/var/log/ingest.offsets", "w")`. Everything the code did *with* the file happened on `m.return_value`, the handle. `write`, `writelines`, `flush` and `close` are auto-created children of that handle, and each keeps its own `call_args_list`, `call_count` and `called`. ## Fetch the handle without disturbing the record The idiom you will see written both ways is: ```python handle = m.return_value # inert: just reads the attribute handle = m() # calls the mock — records an extra open() ``` Both give you the same object, because the handle *is* the return value. But `m()` registers a call to `open` with no arguments, so a later `m.assert_called_once_with(path, "w")` fails with "Called 2 times", and `m.call_count` is off by one. Use `m.return_value` whenever the test also asserts on how `open` was called. If you do use `m()`, at least assert on the open first and fetch the handle afterwards. ## Assert on content, not on chunking `handle.write` records one call per `write()` the code made, so two writes produce two entries in `call_args_list`. Asserting `handle.write.assert_called_once_with(whole_document)` therefore fails the moment the implementation writes a header and a body separately — the test is coupled to an irrelevant detail. The robust assertion rebuilds the document: ```python written = "".join(c.args[0] for c in handle.write.call_args_list) assert written == "shard=0 offset=41\nshard=1 offset=17\n" ``` `call_args_list` holds `call` objects, and each exposes `.args` and `.kwargs`, so `c.args[0]` is the string that was written. When the order of a few specific writes genuinely matters, `handle.write.assert_has_calls([...])` checks that those calls appear as a contiguous run; `assert_any_call` checks membership without order. ## writelines is a different child `f.writelines(lines)` does **not** fan out into individual `write` calls on a mock — the double implements nothing, it only records. `handle.write.call_args_list` stays empty and the single call lands on `handle.writelines` with the whole list as one argument. A test that only joins `write` calls will report an empty document and, if it asserts `written == ""` by accident, will pass. When the code path is not fixed, assert on both children or assert on `m.mock_calls`. ## The whole session in order `m.mock_calls` is the parent record and contains the interleaved script for every handle interaction, spelled with the `call()` prefix: ``` call('/var/log/ingest.offsets', 'w') call().__enter__() call().write('shard=0 offset=41\n') call().__exit__(None, None, None) call().close() ``` That is where to look when the question is ordering — was the file closed before the index was updated, did the code write after an exception — rather than content. Note that `mock_open`'s `__exit__` also triggers `close()`, so a test asserting the file was closed passes for free and proves little. Note too that `assert_has_calls` requires the expected calls to appear *consecutively* in that list, so mixing `__enter__` and `__exit__` into one `assert_has_calls` fails when writes sit between them; use `in m.mock_calls` for a membership check instead. ## The shared-handle trap Every call to the patched `open` returns the same handle, so if the function under test writes two different files, both sets of writes accumulate in one `call_args_list` with nothing to tell them apart. Options are to assert on the combined content when that is meaningful, to give `m` a `side_effect` that returns a distinct double per path, or to test the two writes separately. ## What the assertions cannot tell you The double never encodes, never buffers and never fails. A `str`/`bytes` mismatch, a file opened without the mode the code needs, a partial write, a disk full — none of them can surface here. These assertions verify *what your code asked for*, which is exactly their value and exactly their boundary.
- Why can writing `handle = m()` make m.assert_called_once_with(path, "w") fail?Because `m()` is a call to the double that stands in for `open`, and the double records it. The test now shows two calls — the real one from the code under test and the bookkeeping one from the test itself — so `assert_called_once_with` fails with "Called 2 times". `m.return_value` reads the same handle attribute without registering anything, so it is the safe spelling.
- The implementation switches from repeated write() calls to writelines(). What breaks in your test?Anything that reads `handle.write.call_args_list`, because that list is now empty — the mock does not decompose `writelines` into `write` calls, it records one call on the `writelines` child with the whole list. A test that joined the write calls silently compares against an empty string. Assert on `handle.writelines`, or assert against `m.mock_calls`, which shows whichever method was actually used.
- How would you check that the file was opened before some other collaborator was called?Attach both doubles to a single parent mock — `parent.attach_mock(m, "open_")` and `parent.attach_mock(other, "other")` — and assert on `parent.mock_calls`, which interleaves them in real order. On the `mock_open` double alone, `m.mock_calls` only orders the file interactions themselves: the open, `__enter__`, each write, `__exit__` and the `close` that `__exit__` triggers.
saying these in an interview costs you the question
- Fetches the handle with m() then asserts open called once
- Expects one write call carrying the entire document
- Thinks the handle can read back what was written
- Assumes writelines shows up as individual write calls
- Believes each open() call gets its own write record
- Treats a passing close() assertion as proof of flushing