Why your mock breaks later

Sunday 16 November 2025

An overly aggressive mock may work fine, but break later. Why?

In Why Your Mock Doesn’t Work I explained this rule of mocking:

Mock where the object is used, No Where it is defined.

That blog post explained why that rule was important: Often a mock doesn’t work at all if you do it wrong. But in some cases, the mock will work even if you don’t follow this rule, and then it may break much later. Why?

Let’s say you have code like this:

# user.py

def get_user_settings() -> str:
    with open(Path("~/settings.json").expanduser()) as f:
        return json.load(f)

def add_two_settings() -> int:
    settings = get_user_settings()
    return settings["opt1"] + settings["opt2"]

You write a simple test:

def test_add_two_settings():
    # NOTE: need to create ~/settings.json for this to work:
    #   {"opt1": 10, "opt2": 7}
    assert add_two_settings() == 17

As the comment in the test suggests, the test will only pass if you create the correct settings.json file in your home directory. This is bad: you don’t want to need a complex environment to pass your tests.

What we want to avoid is opening a real file, so mocking it is a natural impulse. open(),

# test_user.py

from io import StringIO
from unittest.mock import patch

@patch("builtins.open")
def test_add_two_settings(mock_open):
    mock_open.return_value = StringIO('{"opt1": 10, "opt2": 7}')
    assert add_two_settings() == 17

Great, the test works without the need to create a file in our home directory!

One day your test suite fails with an error like:

...
  File ".../site-packages/coverage/python.py", line 55, in get_python_source
    source_bytes = read_python_source(try_filename)
  File ".../site-packages/coverage/python.py", line 39, in read_python_source
    return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
TypeErrorreplace() argument 1 must be str, not bytes

What happened!? During your tests, the Coverage.py code runs, called by the Python interpreter. Mock replaces builtin in testing openTherefore any use of it anywhere during testing is affected. In some cases, Coverage.py needs to read your source code to properly record execution. When this happens, Coverage.py inadvertently uses a fake openAnd bad things happen.

When you use a mock, patch it where it is used, not where it is defined. In this case, the patch will:

@patch("myproduct.user.open")
def test_add_two_settings(mock_open):
    ... etc ...

With such a mock, the Coverage.py code will be unaffected.

Keep in mind: It’s not just Coverage.py that can hit this mock. There may be other libraries used by your code, or you can use open
Yourself in another part of your product. definition means to ridicule
Anything The use of the item will be affected. Your intention is to poke fun at only one spot, so target that spot.

To prevent this kind of over-joking, I decided to add some code to coverage.py. There’s a lot of mocking going on, and this issue only appears in Coverage.py with Python 3.14. This isn’t happening to a lot of people yet, but as people start testing with 3.14, it will happen more and more. I didn’t want to have to answer this question multiple times, and I didn’t want to force people to fix their mocks.

From a certain point of view, I shouldn’t do it. They are wrong, not me. But this would reduce the overall friction in the universe. And the solution was really simple:

open = open

This is a top-level statement in my module, so it runs when the module is imported, long before any tests are run. assignment to open will create a global in my module, using the current value of openWhich is found in builtins. This keeps the original safe open Apart from how the builtins can be changed later, for use in my module later.

This is an ad-hoc fix: it only protects one builtin. Mocking other builtins can also break Coverage.py. But open This is normal, and will keep things working smoothly in those cases. And here’s a precedent: I’m already using a more involved technique to protect against mockery. os Module for ten years.

No blog post about mocking is complete without encouraging several other best practices, some of which can get you out of the mocking mess:

#python #testing“feedback



Leave a Comment