Blog
Testing2026-W183 min read

Monkeypatching at the Right Boundary

Your monkeypatch looks correct, the function name resolves, the test still sees the production value. The cause is almost always import-time binding — patch the symbol where the caller resolves it, not where it's defined.

A single brass valve mid-flow on a polished copper pipe assembly against a charcoal background, sharp side lighting catching the valve's machined edges, no people, editorial.

The problem

A pytest test patches a helper function to control its return value, but the test keeps failing because the patched value never appears. The function being patched exists in the module, the monkeypatch call looks correct — and yet the production value keeps showing up.

The approach

The root cause is almost always that the caller has already imported the symbol before the patch runs, so the patch changes the name in the target module but the caller's local reference is unaffected. The fix is to patch at the caller's import boundary, not at the definition.

If module_a.py does from module_b import helper_fn and you monkeypatch module_b.helper_fn, module_a still holds its own reference to the original. You need to patch module_a.helper_fn instead.

A subtler version: helper_fn calls a sub-function internally. You patch the sub-function expecting helper_fn to return a different value — but the real helper_fn is installed in the test environment and runs normally, ignoring your patch of its internal dependency. The fix is to patch helper_fn itself.

# Wrong — patches the implementation detail monkeypatch.setattr(module_a, "_char_fallback_estimate", lambda *a: 500) # Right — patches the function the caller actually calls monkeypatch.setattr(module_a, "estimate_input_tokens", lambda *a, **kw: 500)

What I learned

The rule is: patch the thing the code under test calls, at the module where it resolves the name. When a real package is installed in the test environment (not mocked at the import level), you cannot rely on internal code paths being exercised the same way as in CI without the package. In that situation, the only reliable patch point is the public API boundary — the function your code calls, not the function that function calls.