When the output IS the call
Some functions do not return interesting values, their whole job is a side effect: send the email, charge the card, write the log line. Stubbing cannot test that, because nothing comes back to assert on. You need a double that records what happened to it.
Python ships one in the standard library: unittest.mock.Mock. A Mock object accepts any call, remembers every call, and offers assertion helpers:
m.assert_called_once_with(args)fails unless m was called exactly once with those argumentsm.call_counttells you how many times it was calledMock(return_value=x)makes it double as a stub that returnsx
Same injection trick as lesson 5-2, but now the double is the thing you assert on after the act phase.
Asserting on an outgoing email
notify_user's job is to send a formatted message, so there is no return value to check. The Mock stands in for the email sender, and afterward the test asserts the interaction, namely that it was called once with exactly the right arguments.
from unittest.mock import Mock def notify_user(send, user, message): send(user, f"[HackU] {message}") send_email = Mock() notify_user(send_email, "ada@example.com", "Your build passed") send_email.assert_called_once_with("ada@example.com", "[HackU] Your build passed") print("mock verified the call") print("call count:", send_email.call_count)
Output
mock verified the call
call count: 1notify_user returns None, so a test built around the return value could assert nothing at all. The formatted string is the behavior under test, and the only place it appears is in the argument handed to send.
The assertion checks the [HackU] prefix as part of the expected string, which is what makes this a real test of the formatting rule. A weaker check, such as confirming the mock was called at all, would pass against a version that sent the raw message with no prefix.
Note the injection is the same trick as lesson 5-2, and the difference is where the assert lives. With a stub the assert reads the return value, and with a mock the assert interrogates the double after the act phase, which is a fourth step tacked onto arrange, act, and assert.
The bug a stub could never see
assert_called_once_with catches notify_user accidentally sending the email twice.
A stub answers calls but keeps no diary, so a duplicated send looks identical to a single one from the outside. The mock records every call, which turns a duplicated send, a missed send, and wrong arguments alike into assertable facts.
The word once in the helper name is doing real work. Compare the two ways to write this check:
| Assertion | Catches wrong args | Catches a double send |
|---|---|---|
m.assert_called_with(...) | yes | no |
m.assert_called_once_with(...) | yes | yes |
assert_called_with only inspects the most recent call, so a function that sent twice with the same arguments passes it. Reaching for the once variant by default is the habit worth forming, and dropping to the looser one only when repeated calls are genuinely expected.
Interaction bugs like double-charging a card are exactly what mocks exist to catch, and they are the class of bug users notice fastest.
A mock and a stub in one test
checkout looks up a price and charges the card. get_price is a Mock with a return_value, which makes it a stub, and charge is a plain Mock whose calls the test inspects.
from unittest.mock import Mock def checkout(get_price, charge, user, quantity): price = get_price() charge(user, round(price * quantity, 2)) get_price = Mock(return_value=19.99) charge = Mock() checkout(get_price, charge, "ada", 2) charge.assert_called_once_with("ada", 39.98) print("interaction verified")
Output
interaction verified
Mock(return_value=19.99) is a stub-flavored mock, since every call returns 19.99 and the test never looks at how it was called. The same class covers both roles, which is convenient and also the reason people use the word mock for everything.
The expected value follows from the arithmetic: 19.99 times 2 is 39.98, so the expected call is charge("ada", 39.98). Working that out by hand rather than reusing the code's expression is what keeps the test independent, which is the rule from lesson 3-1.
The round(..., 2) in the implementation matters more than it looks. Floating-point multiplication can produce a value like 39.980000000000004, and asserting against 39.98 would fail without the rounding. That is a real bug the test would expose, and money arithmetic is where it bites hardest.
Two doubles in one test is normal. The price lookup needs to give data, the charge needs to be watched, and choosing per dependency rather than per test is the practical habit.
Naming the third double
A double that is a lightweight but genuinely working implementation is a fake.
The three from lesson 5-1 line up cleanly. Stubs return canned answers, mocks record interactions, and fakes actually behave like the dependency, only cheaper.
A fake is distinguished by having real state and real behavior. An in-memory dictionary standing in for a database can accept a write and then serve that same value back on the next read, which no canned answer can do.
That property is what the next lesson is built on, and you will use one again in the capstone. The order in this unit is deliberate, moving from the simplest double to the one that requires the most code to build.