Can we make our tests fast, reliable, and still human-friendly?
In other words, how do we keep the craft while shaving off the friction? When our code reaches across the network, mocking is often the simplest way to reclaim speed, determinism, and focus without pretending the world is less complex than it is. I will try here to summarize and show some mocking approach I practiced and saw while working at Sunrise. For the sake of the demonstration we will use a practical guide to mocking HTTP clients with unittest.mock
.
Our use case
Let’s anchor everything around a tiny, real client. Something small enough to hold in my head, but realistic enough:
1 | # client.py |
We want our tests to be:
- Fast: not waiting for DNS, TLS handshakes, or remote servers.
- Deterministic: we control the responses, every time.
- Focused: when a test fails, it tells us about our logic not that the network is out or we are facing a rate limit.
To make a long story short, mocking lets us replace requests.get
with a controllable piece of code.
That being said, we will also peek at dependency injection and fixture patterns, because sometimes the cleanest test is the one that does not need patching at all!
Patch once, return JSON
Most of the time, you just need: “When my code calls requests.get(...).json()
, give me this.”
Here’s how:
1 | # test_client.py |
I have to say that at first mocking felt like some dark magic. Reading at the code it is not really obvious why this works. Especially in Python where a lot of abstraction are at play here. Let’s try to break it down:
- First
requests.get(...)
is replaced bymock_get
.return_value
stands for theResponse
object.json.return_value
is our controlledJSON
payload
It is a chain: mock → response → json → payload. Once you see it, it stops feeling magical and starts feeling bit more mechanical.
But let’s try to bring a bit more of clarity and expressiveness. We can actually spell out the response object. This may helps your future-you or your teammate at 2 AM.
1 | from unittest.mock import Mock, patch |
Let’s break it down:
resp = Mock()
, we create the fakeResponse
objectresp.json.return_value = {"ok": True}
, we define what.json()
returnsmock_get.return_value = resp
, we Tellmock_get
to return this object
This exercise may help understanding what is happening under the hood when reading this code:
1 | mock_get.return_value.json.return_value = {"ok": True} |
What if we have multiple requests.get
calls?
Our client calls the requests.get
method two times. At client instantiation with the self.bootstrap
and if needed by using the method get_json()
. Now if I want to test and mock the get_json()
method I will have to be careful. We will need distinct mocked responses in order.
1 | from unittest.mock import Mock, patch |
Think of side_effect
as a tiny queue of your responses. First call → first item. Second call → second item. It is important to understand your code and anticipate on any call that may occur at initialization or by other functions.
Some common pitfalls avoid
1. Patching the wrong import path
Patch where the function is looked up, not where it’s defined.
- If
client.py
doesimport requests
→ patchclient.requests.get
- If it does
from requests import get
→ patchclient.get
Python import always drove me crazy. With the mocking layer it drove me crazier. Knowing may save you some time.
2. Return-chain confusion
1 | # ❌ Won’t work — .json() is a method, not a property |
3. Wrong number or order with side_effect
If you make 2 calls but provide only 1 item? You will hit StopIteration. Keep your lists aligned with same length, same order as the calls you expect.
A small detour with dependency injection
Mocking is great. But sometimes, the cleanest test is the one that does not need a patch at all. Injecting a tiny requester
dependency can help. Let’s modify our client.
1 | from typing import Protocol, Any |
Now our test have predictable objects:
1 | class FakeRequester: |
That being said I would not abstract prematurely. Inject dependencies when they materially improve test ergonomics or production flexibility. Otherwise? A simple patch is perfectly fine. (And often, very elegant.)
Warp up
Mocking is a tool not a dogma. So use real calls when:
- Integration tests: you want to validate wiring, contracts, or server behavior.
- End-to-end tests: you’re exercising the full path, intentionally.
- Testing your own logic: mock the boundary, not the code under test.
Mocking the world does not necessarily make your code better. Mocking the right content does.