Low-Level Design (Object-Oriented Design) Module 7 – Testing your design
Test doubles and seams
Choose between fakes, stubs, mocks and spies, build seams for time, ids and outside services, and keep tests from breaking on every refactoring.
What you will learn
- Choose between dummies, stubs, spies, mocks and fakes for each collaborator of a class
- Create seams for time, randomness, ids and external services with constructor parameters and defaults
- Avoid over-mocking that locks tests to implementation details
- Apply unittest.mock.patch where the code looks a name up, and recognise a patch that misses
- Verify that a fake stays honest by running the real implementation's contract tests against it
Before you start
What you’ll learn · Before you start
What you will learn
- Choose between dummies, stubs, spies, mocks and fakes for each collaborator of a class
- Create seams for time, randomness, ids and external services with constructor parameters and defaults
- Avoid over-mocking that locks tests to implementation details
- Apply unittest.mock.patch where the code looks a name up, and recognise a patch that misses
- Verify that a fake stays honest by running the real implementation's contract tests against it
Before you start
On this page
Test doubles are objects that stand in for a class’s collaborators during a test. An order repository that lives in memory, an SMS gateway that delivers to a list and a clock that the test sets by hand are all test doubles. Seams are the places where code lets its caller choose those collaborators without editing it, most often constructor parameters. Together they let a unit test run in milliseconds and give the same answer on every run, with no database, network or real clock. The choice of double matters as much as the seam. For repositories and gateways, a fake, a small working implementation, is usually the best choice, because the test can check what happened rather than how. Mocks check the calls they receive; they fit outgoing side effects, such as a message that must be sent, and used everywhere else they make tests fail after harmless refactorings.
Five kinds of test double
Gerard Meszaros named five kinds of test double in xUnit Test Patterns, and Martin Fowler’s article Mocks Aren’t Stubs uses the same names. They differ in what the double does and in what the test checks afterwards:
- Dummy: an object passed only to fill a parameter that this test never uses.
- Stub: answers the calls the test expects with values the test chose, and does nothing else.
- Spy: a stub that also records how it was called, so the test can inspect the calls afterwards.
- Mock: an object told in advance which calls to expect, which fails the test when the calls differ.
- Fake: a working implementation with a shortcut, such as a repository that keeps its rows in a dictionary.
| Double | What the test checks | It breaks when |
|---|---|---|
| Dummy | Nothing about it | Rarely |
| Stub | The result of the code under test | The code asks for something new |
| Spy | The calls it recorded | The calls change |
| Mock | The calls it received, against its expectations | The calls change, even if the outcome does not |
| Fake | The state it holds afterwards | It drifts from the real implementation |
Fowler’s article also names the two styles these doubles lead to. State verification checks the outcome, such as what a fake repository holds after the call; behaviour verification checks the calls themselves. The second style suits calls whose only effect is outside the program, and it is the style that ties a test to the code’s current shape.
Seams: where a test double gets in
Michael Feathers, who named the idea in Working Effectively with Legacy Code, defines a seam as a place where a
program’s behaviour can change without editing the code at that place. Every seam has an enabling point, where someone
picks the behaviour. In the shipping service of this lesson the seams are constructor parameters, and there are two
enabling points: main.py in production and the test that builds the service.
Four constructor parameters are four seams
Text description of the diagram
The diagram has three boxes in one column. In the middle is the class under test, ShipmentService, whose constructor takes four parameters: orders, sms, clock and new_id. Each parameter is a seam, a place where whoever builds the service decides which collaborator it gets.
- The box at the top, main.py, in production, lists the real collaborators: SqlOrders, ProviderSms, SystemClock and uuid4. An arrow labelled "passes the real collaborators" leads from it to the service.
- The box at the bottom, "a test", lists the test doubles: InMemoryOrders and SmsOutbox, which are fakes, and FixedClock and ids from a counter, which are stubs. An arrow labelled "passes test doubles" leads from it to the service.
The service's own code is the same in both cases; only the place that builds it changes.
Four kinds of collaborator need seams in almost every design: the clock, randomness, id generation, and anything
outside the process, such as a database, a payment gateway or an SMS provider. Constructor parameters and method
parameters are the plainest seams, because anyone reading the signature sees them. Replacing a module’s name with
unittest.mock.patch also works, but the test then depends on how the code imports what it uses; keep it for code
whose signature you cannot change.
One test, three doubles, one refactoring
ShipmentService.mark_shipped() marks a paid order as shipped and texts the customer the tracking number. Here is
the same test written three times, with a fake, a stub and a mock of the order repository, run against the service
before and after a refactoring that writes only the status column through set_status() instead of saving the whole
order:
three/run_three.py
"""Runs the same test, written with a fake, a stub and a mock, against ShipmentService before and after a refactoring
that changes how it talks to its repository but nothing a customer could notice. Then it shows what the two versions
do, with the fake, to check that the refactoring really is harmless.
"""
import importlib
import sys
import unittest
import shipment
import shipment_refactored
from doubles import InMemoryOrders, SmsOutbox
SUITES = [
("fake: InMemoryOrders", "test_with_fake"),
("stub: StubOrders", "test_with_stub"),
("mock: unittest.mock.Mock", "test_with_mock"),
]
VERSIONS = [("before", shipment), ("after", shipment_refactored)]
def outcome(test_module, version):
"""Runs a test module with `from shipment import ...` resolving to this version of the service."""
sys.modules["shipment"] = version
try:
sys.modules.pop(test_module, None)
tests = unittest.defaultTestLoader.loadTestsFromModule(importlib.import_module(test_module))
result = unittest.TestResult()
tests.run(result)
finally:
sys.modules["shipment"] = shipment
for label, problems in (("error", result.errors), ("fail", result.failures)):
if problems:
last_line = problems[0][1].strip().splitlines()[-1]
return f"{label:<6}{last_line}"
return "pass"
print("The same test with three doubles, before and after the refactoring:\n")
print(f"{'double':<26}{'before':<8}after")
for name, module in SUITES:
before, after = (outcome(module, version) for _, version in VERSIONS)
print(f"{name:<26}{before:<8}{after}")
print("\nWhat the shop and the customer see, both versions run with the fake:")
for label, version in VERSIONS:
orders = InMemoryOrders([shipment.Order(42, "+91 98450 12345", "paid")])
sms = SmsOutbox()
version.ShipmentService(orders, sms).mark_shipped(42, "TRK-7781")
print(f" {label + ':':<8}order 42 is {orders.get(42).status}; SMS: {sms.sent[0][1]}") Output
The same test with three doubles, before and after the refactoring: double before after fake: InMemoryOrders pass pass stub: StubOrders pass error AttributeError: 'StubOrders' object has no attribute 'set_status' mock: unittest.mock.Mock pass fail AssertionError: Expected 'save' to be called once. Called 0 times. What the shop and the customer see, both versions run with the fake: before: order 42 is shipped; SMS: Order 42 has shipped. Tracking number: TRK-7781 after: order 42 is shipped; SMS: Order 42 has shipped. Tracking number: TRK-7781
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 run_three.py
three/test_with_fake.py
import unittest
from doubles import InMemoryOrders, SmsOutbox
from shipment import Order, ShipmentService
PHONE = "+91 98450 12345"
class WithFakes(unittest.TestCase):
def test_paid_order_is_shipped_and_the_customer_is_told(self):
orders = InMemoryOrders([Order(42, PHONE, "paid")])
sms = SmsOutbox()
ShipmentService(orders, sms).mark_shipped(42, "TRK-7781")
self.assertEqual(orders.get(42).status, "shipped") # the state the repository now holds
self.assertEqual(sms.sent, [(PHONE, "Order 42 has shipped. Tracking number: TRK-7781")]) three/test_with_stub.py
import unittest
from doubles import SmsOutbox, StubOrders
from shipment import Order, ShipmentService
PHONE = "+91 98450 12345"
class WithAStub(unittest.TestCase):
def test_paid_order_is_shipped_and_the_customer_is_told(self):
order = Order(42, PHONE, "paid")
sms = SmsOutbox()
ShipmentService(StubOrders(order), sms).mark_shipped(42, "TRK-7781")
self.assertEqual(order.status, "shipped") # the object the stub handed out was changed
self.assertEqual(sms.sent, [(PHONE, "Order 42 has shipped. Tracking number: TRK-7781")]) three/test_with_mock.py
import unittest
from unittest.mock import Mock
from shipment import Order, ShipmentService
PHONE = "+91 98450 12345"
class WithMocks(unittest.TestCase):
def test_paid_order_is_shipped_and_the_customer_is_told(self):
orders = Mock()
orders.get.return_value = Order(42, PHONE, "paid")
sms = Mock()
ShipmentService(orders, sms).mark_shipped(42, "TRK-7781")
orders.save.assert_called_once_with(Order(42, PHONE, "shipped")) # the calls the code made
sms.send.assert_called_once_with(PHONE, "Order 42 has shipped. Tracking number: TRK-7781") three/shipment.py
"""The shipping step of an online shop: a paid order is marked as shipped, and the customer gets an SMS.
ShipmentService receives its two collaborators through its constructor, the seams that let a test choose them.
"""
from dataclasses import dataclass
from typing import Protocol
@dataclass
class Order:
id: int
phone: str
status: str # "paid", then "shipped"
class OrderError(Exception):
"""A request that the rules refuse."""
class Orders(Protocol):
def get(self, order_id: int) -> Order | None: ...
def save(self, order: Order) -> None: ...
def set_status(self, order_id: int, status: str) -> None: ...
class Sms(Protocol):
def send(self, to: str, text: str) -> None: ...
class ShipmentService:
def __init__(self, orders: Orders, sms: Sms):
self._orders = orders
self._sms = sms
def mark_shipped(self, order_id: int, tracking: str) -> None:
order = self._orders.get(order_id)
if order is None:
raise OrderError(f"no order {order_id}")
if order.status != "paid":
raise OrderError(f"order {order_id} is {order.status}, not paid")
order.status = "shipped"
self._orders.save(order)
self._sms.send(order.phone, f"Order {order_id} has shipped. Tracking number: {tracking}") three/shipment_refactored.py
"""The same service after a refactoring: it writes only the status column, through Orders.set_status, instead of
saving the whole order. Nothing a customer or the shop could notice has changed.
"""
from shipment import Order, OrderError, Orders, Sms # noqa: F401 (the same types as before)
class ShipmentService:
def __init__(self, orders: Orders, sms: Sms):
self._orders = orders
self._sms = sms
def mark_shipped(self, order_id: int, tracking: str) -> None:
order = self._orders.get(order_id)
if order is None:
raise OrderError(f"no order {order_id}")
if order.status != "paid":
raise OrderError(f"order {order_id} is {order.status}, not paid")
self._orders.set_status(order_id, "shipped") # one column instead of the whole row
self._sms.send(order.phone, f"Order {order_id} has shipped. Tracking number: {tracking}") three/doubles.py
"""Hand-written test doubles for ShipmentService."""
from dataclasses import replace
class InMemoryOrders:
"""A fake: a working Orders repository that keeps its rows in a dict and hands out copies, as a database does."""
def __init__(self, orders=()):
self._rows = {o.id: replace(o) for o in orders}
def get(self, order_id):
row = self._rows.get(order_id)
return replace(row) if row else None
def save(self, order):
self._rows[order.id] = replace(order)
def set_status(self, order_id, status):
self._rows[order_id] = replace(self._rows[order_id], status=status)
class StubOrders:
"""A stub: get() answers with the order the test chose; save() stores nothing."""
def __init__(self, order):
self.order = order
def get(self, order_id):
return self.order
def save(self, order):
pass
class SmsOutbox:
"""A fake SMS gateway: every message is delivered to a list that the test can read."""
def __init__(self):
self.sent = []
def send(self, to, text):
self.sent.append((to, text)) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
All three tests passed before the change. Afterwards only the fake’s test passed, although the last two lines of the
run show that the shop and the customer see exactly the same outcome. The stub had no set_status(), because it was
written for the calls the code made at the time. The mock expected a call to save() that no longer happens. Neither
red result found a bug; each one only reported that the code had changed shape.
Practise: fakes for a checkout service
A checkout service charges a card through a payment gateway and keeps orders in a repository. Write a fake of each, in Python or JavaScript: a gateway that declines the cards it is told to and never charges a retried key twice, and a repository that hands out copies, as a database does. The sample tests run checkout scenarios with your fakes.
Exercise · Medium · Python, JavaScript
Write the fakes that a checkout service is tested with
A checkout service charges a card through a payment gateway and keeps its orders in a repository. Both arrive through its constructor, so a test can pass in fakes: small working versions that run in memory. Write the two fakes in fakes.py (fakes.mjs in JavaScript). The comment at the top of the file gives every method; these are the rules they must keep:
- The gateway numbers new charges
ch_1,ch_2and so on. A declined card raisesPaymentDeclined(throws, in JavaScript) and is charged nothing. A key that was used before returns the first charge's id and charges nothing more, so a retried checkout never charges twice.total_charged(card)(totalCharged) adds up what a card paid. - The repository hands out ids 1, 2, 3 and so on from
next_id()(nextId()). It stores a copy of every order it saves and returns a copy fromget()andfor_customer()(forCustomer()), the way a database does: changing an order you got changes nothing until you save it. An unknown id givesNone(null), andfor_customer()lists one customer's orders sorted by id.
The sample tests are checkout scenarios that use your fakes: a paid checkout, a declined card, a retry with the same key, two separate checkouts, copies in and out, and a cancellation. The service itself is in the test file; it does not change.
Python · Starter code · fakes.py
"""Fakes for the checkout service: a payment gateway and an order repository that work, in memory.
FakePaymentGateway(declined_cards=())
charge(card, amount, key) the charge id, "ch_1", "ch_2", … in the order new charges are made. A card in
declined_cards raises PaymentDeclined and charges nothing. A key that was used before
returns the first charge's id and charges nothing more.
total_charged(card) the sum of the amounts charged to the card (0 for a card never charged)
InMemoryOrders()
next_id() 1, then 2, 3, … : a new order id on every call
save(order) stores a copy of the order (orders are dataclasses: dataclasses.replace copies one)
get(order_id) a copy of the stored order, or None
for_customer(customer) copies of the customer's orders, sorted by id
"""
from dataclasses import replace # noqa: F401 (you will need it)
class PaymentDeclined(Exception):
"""The card was declined."""
class FakePaymentGateway:
def __init__(self, declined_cards=()):
raise NotImplementedError
def charge(self, card, amount, key):
raise NotImplementedError
def total_charged(self, card):
raise NotImplementedError
class InMemoryOrders:
def __init__(self):
raise NotImplementedError
def next_id(self):
raise NotImplementedError
def save(self, order):
raise NotImplementedError
def get(self, order_id):
raise NotImplementedError
def for_customer(self, customer):
raise NotImplementedError The sample tests · test_fakes.py
"""Checkout scenarios run with your fakes. CheckoutService is the code under test; it gets its gateway and its order
repository through its constructor, so these tests can pass in yours.
"""
from dataclasses import dataclass
from fakes import FakePaymentGateway, InMemoryOrders, PaymentDeclined
@dataclass
class Order:
id: int
customer: str
amount: int # paise
charge_id: str
status: str # "paid" or "cancelled"
class CheckoutError(Exception):
pass
class CheckoutService:
def __init__(self, gateway, orders):
self._gateway = gateway
self._orders = orders
def checkout(self, customer, card, amount, key):
"""Charges the card and stores a paid order; a retry with the same key returns the same order."""
charge_id = self._gateway.charge(card, amount, key)
for order in self._orders.for_customer(customer):
if order.charge_id == charge_id:
return order.id
order = Order(self._orders.next_id(), customer, amount, charge_id, "paid")
self._orders.save(order)
return order.id
def cancel(self, order_id):
order = self._orders.get(order_id)
if order is None or order.status != "paid":
raise CheckoutError(f"order {order_id} cannot be cancelled")
order.status = "cancelled"
self._orders.save(order)
def make(declined_cards=()):
gateway, orders = FakePaymentGateway(declined_cards), InMemoryOrders()
return CheckoutService(gateway, orders), gateway, orders
def test_checkout_charges_once_and_stores_a_paid_order():
"""a checkout charges the card once and stores a paid order"""
shop, gateway, orders = make()
order_id = shop.checkout("asha", "card-1", 49_900, key="k1")
assert gateway.total_charged("card-1") == 49_900
assert orders.get(order_id) == Order(order_id, "asha", 49_900, "ch_1", "paid")
def test_declined_card_stores_no_order():
"""a declined card raises PaymentDeclined, charges nothing and stores no order"""
shop, gateway, orders = make(declined_cards={"card-9"})
try:
shop.checkout("ravi", "card-9", 12_000, key="k1")
raised = False
except PaymentDeclined:
raised = True
assert raised
assert gateway.total_charged("card-9") == 0
assert orders.for_customer("ravi") == []
def test_retry_with_the_same_key_charges_once():
"""a retry with the same key charges once and returns the same order"""
shop, gateway, orders = make()
first = shop.checkout("asha", "card-1", 49_900, key="k1")
again = shop.checkout("asha", "card-1", 49_900, key="k1")
assert again == first
assert gateway.total_charged("card-1") == 49_900
assert len(orders.for_customer("asha")) == 1
def test_two_keys_make_two_charges_and_two_orders():
"""two checkouts with different keys make charges ch_1 and ch_2 and orders 1 and 2"""
shop, gateway, orders = make()
assert shop.checkout("asha", "card-1", 10_000, key="k1") == 1
assert shop.checkout("asha", "card-1", 25_000, key="k2") == 2
assert gateway.total_charged("card-1") == 35_000
assert [o.charge_id for o in orders.for_customer("asha")] == ["ch_1", "ch_2"]
def test_get_returns_a_copy():
"""changing an order you got does not change the stored one until it is saved"""
_, _, orders = make()
orders.save(Order(1, "asha", 10_000, "ch_1", "paid"))
order = orders.get(1)
order.status = "cancelled"
assert orders.get(1).status == "paid"
orders.save(order)
assert orders.get(1).status == "cancelled"
def test_save_keeps_a_copy():
"""changing an order after saving it does not change the stored one"""
_, _, orders = make()
order = Order(1, "asha", 10_000, "ch_1", "paid")
orders.save(order)
order.amount = 1
assert orders.get(1).amount == 10_000
def test_cancel_and_list_by_customer():
"""cancel stores the new status; for_customer lists only that customer's orders, sorted by id"""
shop, _, orders = make()
a1 = shop.checkout("asha", "card-1", 10_000, key="k1")
shop.checkout("ravi", "card-2", 20_000, key="k2")
a2 = shop.checkout("asha", "card-1", 30_000, key="k3")
shop.cancel(a1)
assert [(o.id, o.status) for o in orders.for_customer("asha")] == [(a1, "cancelled"), (a2, "paid")]
assert orders.get(99) is None JavaScript · Starter code · fakes.mjs
/**
* Fakes for the checkout service: a payment gateway and an order repository that work, in memory.
*
* new FakePaymentGateway(declinedCards = [])
* charge(card, amount, key) the charge id, "ch_1", "ch_2", … in the order new charges are made. A card in
* declinedCards throws PaymentDeclined and charges nothing. A key that was used before
* returns the first charge's id and charges nothing more.
* totalCharged(card) the sum of the amounts charged to the card (0 for a card never charged)
*
* new InMemoryOrders()
* nextId() 1, then 2, 3, … : a new order id on every call
* save(order) stores a copy of the order (orders are plain objects: { ...order } copies one)
* get(orderId) a copy of the stored order, or null
* forCustomer(customer) copies of the customer's orders, sorted by id
*/
export class PaymentDeclined extends Error {}
export class FakePaymentGateway {
constructor(declinedCards = []) {
throw new Error('not written yet');
}
charge(card, amount, key) {
throw new Error('not written yet');
}
totalCharged(card) {
throw new Error('not written yet');
}
}
export class InMemoryOrders {
constructor() {
throw new Error('not written yet');
}
nextId() {
throw new Error('not written yet');
}
save(order) {
throw new Error('not written yet');
}
get(orderId) {
throw new Error('not written yet');
}
forCustomer(customer) {
throw new Error('not written yet');
}
} The sample tests · fakes.test.mjs
// Checkout scenarios run with your fakes. CheckoutService is the code under test; it gets its gateway and its order
// repository through its constructor, so these tests can pass in yours.
import { test, assert } from 'mysmartcopilot:test';
import { FakePaymentGateway, InMemoryOrders, PaymentDeclined } from './fakes.mjs';
class CheckoutError extends Error {}
class CheckoutService {
constructor(gateway, orders) {
this.gateway = gateway;
this.orders = orders;
}
/** Charges the card and stores a paid order; a retry with the same key returns the same order. */
checkout(customer, card, amount, key) {
const chargeId = this.gateway.charge(card, amount, key);
for (const order of this.orders.forCustomer(customer)) {
if (order.chargeId === chargeId) return order.id;
}
const order = { id: this.orders.nextId(), customer, amount, chargeId, status: 'paid' };
this.orders.save(order);
return order.id;
}
cancel(orderId) {
const order = this.orders.get(orderId);
if (order === null || order.status !== 'paid') throw new CheckoutError(`order ${orderId} cannot be cancelled`);
order.status = 'cancelled';
this.orders.save(order);
}
}
function make(declinedCards = []) {
const gateway = new FakePaymentGateway(declinedCards);
const orders = new InMemoryOrders();
return { shop: new CheckoutService(gateway, orders), gateway, orders };
}
test('a checkout charges the card once and stores a paid order', () => {
const { shop, gateway, orders } = make();
const id = shop.checkout('asha', 'card-1', 49900, 'k1');
assert.equal(gateway.totalCharged('card-1'), 49900);
assert.deepEqual(orders.get(id), { id, customer: 'asha', amount: 49900, chargeId: 'ch_1', status: 'paid' });
});
test('a declined card throws PaymentDeclined, charges nothing and stores no order', () => {
const { shop, gateway, orders } = make(['card-9']);
assert.throws(() => shop.checkout('ravi', 'card-9', 12000, 'k1'), PaymentDeclined);
assert.equal(gateway.totalCharged('card-9'), 0);
assert.deepEqual(orders.forCustomer('ravi'), []);
});
test('a retry with the same key charges once and returns the same order', () => {
const { shop, gateway, orders } = make();
const first = shop.checkout('asha', 'card-1', 49900, 'k1');
const again = shop.checkout('asha', 'card-1', 49900, 'k1');
assert.equal(again, first);
assert.equal(gateway.totalCharged('card-1'), 49900);
assert.equal(orders.forCustomer('asha').length, 1);
});
test('two checkouts with different keys make charges ch_1 and ch_2 and orders 1 and 2', () => {
const { shop, gateway, orders } = make();
assert.equal(shop.checkout('asha', 'card-1', 10000, 'k1'), 1);
assert.equal(shop.checkout('asha', 'card-1', 25000, 'k2'), 2);
assert.equal(gateway.totalCharged('card-1'), 35000);
assert.deepEqual(orders.forCustomer('asha').map((o) => o.chargeId), ['ch_1', 'ch_2']);
});
test('changing an order you got does not change the stored one until it is saved', () => {
const { orders } = make();
orders.save({ id: 1, customer: 'asha', amount: 10000, chargeId: 'ch_1', status: 'paid' });
const order = orders.get(1);
order.status = 'cancelled';
assert.equal(orders.get(1).status, 'paid');
orders.save(order);
assert.equal(orders.get(1).status, 'cancelled');
});
test('changing an order after saving it does not change the stored one', () => {
const { orders } = make();
const order = { id: 1, customer: 'asha', amount: 10000, chargeId: 'ch_1', status: 'paid' };
orders.save(order);
order.amount = 1;
assert.equal(orders.get(1).amount, 10000);
});
test("cancel stores the new status; forCustomer lists only that customer's orders, sorted by id", () => {
const { shop, orders } = make();
const a1 = shop.checkout('asha', 'card-1', 10000, 'k1');
shop.checkout('ravi', 'card-2', 20000, 'k2');
const a2 = shop.checkout('asha', 'card-1', 30000, 'k3');
shop.cancel(a1);
assert.deepEqual(orders.forCustomer('asha').map((o) => [o.id, o.status]), [[a1, 'cancelled'], [a2, 'paid']]);
assert.equal(orders.get(99), null);
}); A hint
Keep three things in the gateway: the declined cards as a set, a dictionary from each key to the charge id it got, and a list of the charges made. Check the key first: a retry returns the old id before anything else happens. For the repository, keep a dictionary from id to a copy of each order (dataclasses.replace(order) in Python, { ...order } in JavaScript), and copy again on the way out.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Seams for time, chance and outside services, with the patching traps reproduced Pro: opens with a Pro, Premium or Ultimate pass
A clock or a random code can reach a test through a constructor parameter or through a patched module name. Here one login-code service is tested both ways, a patch that misses its target is reproduced, and six kinds of seam are compared with their costs.
In this part:
- A patched clock next to an injected one, test for test
- A patch that misses its target, reproduced
- Seams for ids, randomness and outside services
- Six kinds of seam, compared
2 runnable examples (compare.py, otp.py, otp_fromimport.py, otp_hardwired.py, patch_misses.py, test_otp_injected.py, test_otp_patched.py) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Low-level design: parking lot (the model)
Over-mocking, measured: false alarms, missed bugs and honest fakes Pro: opens with a Pro, Premium or Ultimate pass
Doubles that check calls fail when the code changes how it works, and they can stay green when the outcome is wrong. This part counts both for four suites over eight changed versions of one service, then shows a mock that hides an API change and a fake that hides a bug.
In this part:
- Four suites, eight changes: the numbers
- A mock that accepts calls the real client refuses
- A fake that lies, and the contract that catches it
- Choosing a double, with the trade-offs
3 runnable examples (Python, matrix.py, orders_contract.py, repos.py, run_contract.py, suites.py, variants.py) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is the difference between a stub and a mock, and which would you use for an order repository?
A stub answers the calls a test expects with values the test chose and checks nothing, so the test asserts on the result of the code under test. A mock is told in advance which calls to expect and fails the test when the calls it receives differ, so the test asserts on the interaction itself. For an order repository neither is the first choice: a fake, an in-memory repository that really stores and returns orders, lets the test check the state the repository ends with, and it keeps passing when the code changes how it reads and writes. A stub suits a single read when no fake exists. Interaction checks, with a fake outbox, a spy or an autospec mock, belong to outgoing side effects such as an SMS, where the call is the behaviour and nothing else can observe it.
-
Mid to senior
After a refactoring that changed no behaviour, 40 of your team's 120 service tests failed. They mock the repository and assert every call it receives. What do you change, and what stays mocked?
-
Fresher to mid
A coupon class calls time.time() and uuid.uuid4() inside its methods, and its 15-minute expiry rule has no tests. How do you make it testable without patching, and what do you do if you may not change its constructor?
-
Mid to senior
Every test passes with your in-memory fake repository, but in production some status changes are lost. How could the fake be hiding the bug, and how do you keep fakes honest?
Key takeaways
- A test double takes a collaborator’s place in a test; a seam is where the code lets its caller choose that collaborator.
- Dummies fill parameters, stubs answer, spies record, mocks check calls, and fakes work, with a shortcut.
- Prefer fakes for repositories and gateways: they let tests check outcomes, which survive refactorings.
- Keep interaction checks for outgoing side effects such as an SMS, where the call is the behaviour.
- Make seams for the clock, randomness, ids and outside services with constructor parameters and defaults.
References
- Mocks Aren't Stubs (Martin Fowler)
- TestDouble (Martin Fowler)
- unittest.mock: mock object library (Python Software Foundation)
- unittest.mock: Where to patch (Python Software Foundation)
- unittest.mock: Autospeccing (Python Software Foundation)
- time: Time access and conversions (time.monotonic) (Python Software Foundation)
- secrets: Generate secure random numbers for managing secrets (Python Software Foundation)
- dataclasses: replace() (Python Software Foundation)
- sqlite3: DB-API 2.0 interface for SQLite databases (Python Software Foundation)
- Software Engineering at Google, chapter 13: Test Doubles (O’Reilly Media (published online by the Abseil project))
- Working Effectively with Legacy Code (Michael Feathers) (Pearson (InformIT))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress