DEV Community

Taylor Wang
Taylor Wang

Posted on

I Trusted a Remote Green Suite for 48 Hours. The Mock Bound the Wrong Name.

Have you ever watched a remote pytest job go green, then failed the same test file on your laptop? I spent forty-eight hours on that mismatch, and the logs never named the real function binding. The remote agent kept editing the mock target string, and I kept trusting a color on a dashboard. The bug was an import-time name, not a network flake, a pytest plugin, or a bad wheel.

I am writing this as field notes, not as a victory lap or a polished postmortem slide. You will see the layout I reproduced, the commands I ran, and the table I now keep nearby. Nothing here depends on a private customer story, a secret metric, or an unpublished benchmark. If you already know where unittest.mock.patch binds, you can skip ahead to the checklist and steal that part.

The background I carried into hour zero

I had a small notifier that fetched a forecast and then formatted a single line of text. The fetch helper lived in weather_client.py, and the notifier imported that helper at module load. Does a one-line import look too boring to justify a weekend of screenshots and sticky notes? It looked boring to me too, which is why I wasted the first night on dependency trees.

The local failure was loud in a way that felt like infrastructure, not like a name-binding mistake. pytest reported a connection error from the test that was supposed to own the network boundary. I assumed the remote box had different certificates, a quieter resolver, or a helper library that swallowed the call. I did not assume the mock decorator had been decorating a function the suite never called.

Hours 0–8: I trusted the remote color

I pasted the failure into MonkeyCode, using its free model access and free server option, then waited. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The remote pytest run came back green, and the agent claimed the network was fully mocked out. Was I going to re-read the import graph, or was I going to celebrate a clean remote log? I celebrated, pushed nothing, and then watched the same test hit the network on my machine.

What I tried in that first block of hours looked busy, and it still missed the binding:

  • I copied pip freeze from the remote session and diffed it against my local virtualenv output.
  • I reran pytest -q tests/test_notifier.py locally until the traceback felt memorized rather than read.
  • I asked the agent to tighten the mock, and it kept patching weather_client.fetch_forecast with fresh confidence.
  • I added a five-second sleep, because tired people still bargain with flakes instead of reading imports.

None of those moves printed the object identity of the function the notifier actually held. That omission cost me the rest of the day, and then most of the night.

Hours 8–20: I compared the wrong inventories

Why do we always inventory packages before we inventory names? I printed interpreter paths, then pytest versions, then httpx versions, then the working directory of each run. The lists matched closely enough that I started inventing filesystem theories I could not prove. I even compared sys.path entries, because a stray editable install has burned me before on other weekends.

python -c "import sys, pytest, httpx; print(sys.executable); print(pytest.__version__); print(httpx.__version__)"
python -m pytest --collect-only tests/test_notifier.py
python -c "import os; print(os.getcwd()); print(os.listdir('.'))"
Enter fullscreen mode Exit fullscreen mode

The collection output named the same test on both machines, which made the green remote run feel even more authoritative. Have you noticed how a matching node id can talk you out of reading the module under test? It talked me out of that reading for another twelve hours. I still had no proof that the patched name and the called name were the same object.

Hours 20–32: I printed every interpreter I could find

I have a habit, after enough bad weekends, of proving which Python binary actually ran the test. That habit is useful, and it was still the wrong habit for this failure. I checked shebangs, activated the venv twice, and ran pytest as a module so the path could not drift. The binary was stable. The mock target string was stable too, which should have been a clue.

which python
head -n 1 "$(which pytest)"
python -m pytest -q -s tests/test_notifier.py
python -c "import notifier, weather_client; print(notifier.fetch_forecast); print(weather_client.fetch_forecast)"
Enter fullscreen mode Exit fullscreen mode

That last command is the one I should have run at hour two, not hour twenty-six. The two printed function objects were not the same, and the ids made the remote green result look suddenly cheap. Can a remote suite stay green while your local process calls a live function through an old name? Yes, if the test patches a module the production code already copied from.

Hours 32–48: I finally printed the bound name

The notifier did not look up weather_client.fetch_forecast at call time. It bound the helper once, at import, with a perfectly ordinary from import. The test patched the definition site, so the live name inside notifier never moved. The remote run stayed green because the agent later stubbed a second helper that the remote test imported in a different order. My laptop never loaded that extra stub, so the original function still went to the network.

I reproduced the shape with three files and no third-party weather API. You should be able to paste this layout into an empty directory and get the same lie from pytest. Label this as a reconstructed example, not as a claim about any hosted service I cannot show you.

Layout

weather_demo/
  weather_client.py
  notifier.py
  tests/test_notifier.py
Enter fullscreen mode Exit fullscreen mode

weather_client.py

# Reconstructed example: a helper that looks expensive to call.

def fetch_forecast(city: str) -> dict:
    raise RuntimeError(f"network was reached for city={city!r}")
Enter fullscreen mode Exit fullscreen mode

notifier.py

from weather_client import fetch_forecast


def format_alert(city: str) -> str:
    payload = fetch_forecast(city)
    return f"{city}: {payload['summary']}"
Enter fullscreen mode Exit fullscreen mode

tests/test_notifier.py

from unittest.mock import patch

from notifier import format_alert


@patch("weather_client.fetch_forecast")
def test_format_alert_avoids_network(mock_fetch):
    mock_fetch.return_value = {"summary": "clear"}
    assert format_alert("Oslo") == "Oslo: clear"
Enter fullscreen mode Exit fullscreen mode

Run it and watch the mock miss:

cd weather_demo
python -m pytest -q tests/test_notifier.py
Enter fullscreen mode Exit fullscreen mode

You should see RuntimeError: network was reached for city='Oslo'. The decorator looks precise, and it still wraps a name the notifier no longer consults. Patch the name the production module actually holds, and the test becomes honest:

@patch("notifier.fetch_forecast")
def test_format_alert_avoids_network(mock_fetch):
    mock_fetch.return_value = {"summary": "clear"}
    assert format_alert("Oslo") == "Oslo: clear"
Enter fullscreen mode Exit fullscreen mode

I now print both objects before I believe a green remote job. The extra six lines would have saved the entire second night.

import notifier
import weather_client

print("client", id(weather_client.fetch_forecast), weather_client.fetch_forecast)
print("notifier", id(notifier.fetch_forecast), notifier.fetch_forecast)
print("same object", notifier.fetch_forecast is weather_client.fetch_forecast)
Enter fullscreen mode Exit fullscreen mode

Decision table I keep beside the laptop

What the production module does Patch string that actually binds Patch string that only looks correct What you will observe
import weather_client then weather_client.fetch_forecast() weather_client.fetch_forecast notifier.fetch_forecast Local calls may still hit the helper.
from weather_client import fetch_forecast at import time notifier.fetch_forecast weather_client.fetch_forecast Remote green, local RuntimeError.
Late import inside the function body The module used at the call site A helper module never imported The mock can pass on both machines.
Attribute lookup on a class instance The path where the instance is created The class definition in another file You will debug fixtures instead of names.

I treat that table as a preflight, not as folklore I recite after the outage. If the production file uses a from import, I refuse to patch the definition module first. If the remote agent proposes the definition module anyway, I print object ids before I accept the color of the build.

Commands I will run before I trust another remote green job

  1. Print sys.executable, then run pytest as python -m pytest so the binary cannot drift.
  2. Print id() for the helper in the definition module and in the calling module.
  3. Fail the helper with an explicit RuntimeError, so a missed mock cannot look like a clean skip.
  4. Read the production import line out loud, including whether it is import or from.
  5. Only then compare remote and local traces, because color is not a binding proof.
python -m pytest -q -s tests/test_notifier.py --tb=short
python -c "import notifier, weather_client as wc; print(notifier.fetch_forecast is wc.fetch_forecast)"
Enter fullscreen mode Exit fullscreen mode

Would I still use a free remote server for this kind of loop? Yes, as a second pair of traces, not as a source of truth. The useful part was having another process run the same file while I printed names locally. The failure mode was treating that process as a judge of patch strings it never proved.

What broke, and what I would repeat

The thing that broke was trust, not httpx, and not pytest collection order as a first cause. I let a green remote suite stand in for a proof that two names pointed at one function object. I also let the agent keep mutating the mock string instead of printing the bound object after each edit. That is a process bug I can repeat in any editor, with or without a remote box.

What I would repeat is the small layout, the object-id print, and the decision table above. I would still paste a failing traceback into a model when I want a second draft of a test. I would not accept a remote green result until notifier.fetch_forecast is weather_client.fetch_forecast matches the import style in the production file. That single boolean is a better receipt than a dashboard color.

If you want a second process for those traces without standing up another laptop, MonkeyCode’s free server option is enough to rerun this checklist. I would still keep the proof on my own terminal, because that is where the bound name actually lives.

Limitations, and who should not use this loop

This workflow is a name-binding check, not a substitute for hermetic continuous integration. It will not tell you about DNS, TLS, rate limits, or the real forecast vendor. It also will not freeze a model, a quota, or a machine profile, and I am not claiming any of those details here. Treat the remote run as an extra log, then make the local process prove the mock.

Do not use this approach if you need an auditable production-identical runner, a guaranteed cleanroom, or a shared staging network. Do not use it if your suite must never touch an interpreter you do not pin yourself. Do not use it if you cannot read the production import line, because the table above will not save a test you refuse to open. And do not copy a mock string from an agent into a module you have not printed with id().

I wasted forty-eight hours proving a fact that Python will tell you in one comparison. The next time a remote suite looks braver than my laptop, I am printing the bound name first. Are you still debugging the network, or are you ready to print the function your module actually called?

Top comments (0)