ЁЯПл The SchoolтА║ЁЯУЭ TestingтА║ЁЯзСтАНЁЯПл рдзрдбрд╛ 05 тАФ Test doubles: рдмрджрд▓реА рдкрд░реАрдХреНрд╖рдХ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯзСтАНЁЯПл рдзрдбрд╛ 05 тАФ Test doubles: рдмрджрд▓реА рдкрд░реАрдХреНрд╖рдХ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-fixtures ┬╖ рдкреБрдвреЗ: lesson-06-integration


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ04, рдЖрдгрд┐ test doubles: unit test рдиреЗ рдЦрд▒реНрдпрд╛ рд╕реНрд╡рд░реВрдкрд╛рдд рд╡рд╛рдкрд░реВ рдирдпреЗрдд рдЕрд╢рд╛ рднрд╛рдЧрд╛рдВрдЪреНрдпрд╛ рдЬрд╛рдЧреА рд╡рд╛рдкрд░рд▓реЗрд▓реЗ рдкрд░реНрдпрд╛рдп. exam/grades.py рдордзрд▓рд╛ AttendanceClient attendance service рд▓рд╛ (рджреБрд╕рд░рд╛ server) рд╡рд┐рдЪрд╛рд░рддреЛ рдХреА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдХрд┐рддреА рджрд┐рд╡рд╕ рдЖрд▓реА. Tests рддреНрдпрд╛ service рдЪреНрдпрд╛ рдЬрд╛рдЧреА stub, fake, spy рдХрд┐рдВрд╡рд╛ mock рд╡рд╛рдкрд░рддрд╛рдд тАФ рдЖрдгрд┐ over-mocking рдореБрд│реЗ test рдЪреБрдХреАрдЪреА рдЧреЛрд╖реНрдЯ рдХреБрдареЗ рддрдкрд╛рд╕рддреЗ рддреЗ рд╢рд┐рдХрддрд╛рдд. exam/lab.py рдордзрд▓реЗ StubTransport, FakeAttendance рдЖрдгрд┐ SpyTransport, standard library рдордзрд▓реЗ unittest.mock, рдЖрдгрд┐ exam/demo.py рдордзрд▓реЗ doubles().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдЦрд▒реНрдпрд╛ attendance рдкрд░реАрдХреНрд╖рдХ, рд╕реМ. рд░рд╛рд╡, рджреБрд╕рд▒реНрдпрд╛ рдЗрдорд╛рд░рддреАрдд рдмрд╕рддрд╛рдд. рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рд╢реНрдирд╛рд╕рд╛рдареА рддрд┐рдереЗ рдЪрд╛рд▓рдд рдЬрд╛рдпрд▓рд╛ рдЦреВрдк рд╡реЗрд│ рд▓рд╛рдЧрддреЛ, рдЖрдгрд┐ рдХрд╛рд╣реА рджрд┐рд╡рд╕ рддреНрдпрд╛ рд░рдЬреЗрд╡рд░ рдЕрд╕рддрд╛рдд. рдореНрд╣рдгреВрди рдХрддрд░рд┐рдирд╛ рдмрджрд▓реА рд╡рд╛рдкрд░рддреЗ. ЁЯзСтАНЁЯПл

рдордЧ рджреАрдкрд┐рдХрд╛ рдЙрддреНрддрд░ рди рдмрджрд▓рддрд╛ рдкреНрд░рд╢реНрдирд╛рдЪреЗ рд╢рдмреНрдж рдмрджрд▓рддреЗ ("term 1 рд╕рд╛рдареА, рдХреГрдкрдпрд╛"). Fake рддрд░реАрд╣реА рдЙрддреНрддрд░ рджреЗрддреЗ: test pass. тЬЕ Mock рд▓рд╛ рдЬреБрдиреЗ рд╢рдмреНрдж рдЕрдкреЗрдХреНрд╖рд┐рдд рд╣реЛрддреЗ: test fail. тЭМ Mock code рдиреЗ рдХрд╕реЗ рд╡рд┐рдЪрд╛рд░рд▓реЗ рддреЗ рддрдкрд╛рд╕рдд рд╣реЛрддреЗ, рддреНрдпрд╛рдиреЗ рдХрд╛рдп рдирд┐рд╖реНрдХрд░реНрд╖ рдХрд╛рдврд▓рд╛ рддреЗ рдирд╛рд╣реА.

рдЖрдгрд┐ рдПрдХреЗ рджрд┐рд╡рд╢реА stub рдЪреБрдХреАрдЪреНрдпрд╛ рд╢реАрд░реНрд╖рдХрд╛рдЪреЗ рдХрд╛рд░реНрдб рджрд╛рдЦрд╡рддреЗ. рдЦрд▒реНрдпрд╛ рд╕реМ. рд░рд╛рд╡ рдХрдзреАрдЪ "present" рд▓рд┐рд╣реАрдд рдирд╛рд╣реАрдд тАФ рддреНрдпрд╛ "days_present" рд▓рд┐рд╣рд┐рддрд╛рдд. Test pass рдЭрд╛рд▓реА; рдЦрд░реА рд╢рд╛рд│рд╛ рдореЛрдбрддреЗ. ЁЯШм

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    c["ЁЯУЗ AttendanceClient<br/>eligible if at least 75%"] --> t{"transport"}
    t -->|"production"| real["ЁЯПв real attendance service"]
    t -->|"stub"| s["canned 172/200 тЖТ 86.0%"]
    t -->|"fake"| f["working dict<br/>42: 86.0% ┬╖ 7: 70.0%"]
    t -->|"spy"| sp["records calls<br/>/attendance/42"]
    t -->|"mock"| m["expects exact call<br/>fails on ?term=1"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/testing/lesson-diagrams.html#l05

тЭУ рдХрд╛рдп

рд╣реА рдирд╛рд╡реЗ Gerard Meszaros рдпрд╛рдВрдЪреНрдпрд╛ xUnit Test Patterns (2007) рдордзреВрди рдЖрд▓реА рдЖрд╣реЗрдд; Martin Fowler рдпрд╛рдВрдЪрд╛ рдирд┐рдмрдВрдз Mocks Aren't Stubs рд╣рд╛ рдлрд░рдХ рдЫрд╛рди рд╕рдордЬрд╛рд╡рддреЛ.

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг unit test рд╡реЗрдЧрд╡рд╛рди рдЖрдгрд┐ рдЦрд╛рддреНрд░реАрд╢реАрд░ рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реА, рдЖрдгрд┐ рдЦрд░реА service рджреЛрдиреНрд╣реАрдкреИрдХреА рдХрд╛рд╣реАрдЪ рдирд╛рд╣реА: рддреА network рд╡рд░ рдЖрд╣реЗ (distributed-systems рд╢рд╛рд│реЗрдЪрд╛ рдзрдбрд╛ 01), рддреА рдмрдВрдж рдЕрд╕реВ рд╢рдХрддреЗ, рдЖрдгрд┐ рддрд┐рдЪреНрдпрд╛рдХрдбреЗ рддреБрдордЪреНрдпрд╛ рдирд┐рдпрдВрддреНрд░рдгрд╛рдд рдирд╕рд▓реЗрд▓рд╛ data рдЕрд╕рддреЛ. Doubles рдореБрд│реЗ "503 рд╡рд░ рдХрд╛рдп рд╣реЛрддреЗ?" рд╣реЗ рддреБрдореНрд╣реА рдПрдХрд╛ millisecond рдордзреНрдпреЗ рд╡рд┐рдЪрд╛рд░реВ рд╢рдХрддрд╛. рдкрдг рдкреНрд░рддреНрдпреЗрдХ double рд╣рд╛ рдЦрд▒реНрдпрд╛ рдЧреЛрд╖реНрдЯреАрдмрджреНрджрд▓рдЪрд╛ рдПрдХ рджрд╛рд╡рд╛ рдЖрд╣реЗ. рдХреЛрдгреАрдЪ рди рддрдкрд╛рд╕рд▓реЗрд▓рд╛ рджрд╛рд╡рд╛ рдЪреБрдХреАрдЪрд╛ рдЕрд╕реВ рд╢рдХрддреЛ тАФ рдзрдбрд╛ 07 рддреЛ рддрдкрд╛рд╕рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

exam/grades.py рдордзрд▓рд╛ AttendanceClient(transport) transport("/attendance/<id>") рдмреЛрд▓рд╡рддреЛ рдЖрдгрд┐ (status, body) рдЕрдкреЗрдХреНрд╖рд┐рдд рдзрд░рддреЛ. 404 рд╡рд░ рддреЛ StudentNotFound рджреЗрддреЛ, 5xx рд╡рд░ AttendanceUnavailable, рдЖрдгрд┐ рдЕрдиреНрдпрдерд╛ days_present ├Ч 100 ├╖ days_total рдореЛрдЬрддреЛ, рдПрдХрд╛ рджрд╢рд╛рдВрд╢рд╛рдкрд░реНрдпрдВрдд round рдХрд░реВрди. exam/lab.py рдордзреНрдпреЗ, StubTransport(status, body) рдиреЗрд╣рдореА рддреАрдЪ рдЬреЛрдбреА рдкрд░рдд рджреЗрддреЛ, FakeAttendance рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрдЪреА dict рдареЗрд╡рддреЛ рдЖрдгрд┐ рдЕрдиреЛрд│рдЦреАрдВрд╕рд╛рдареА 404 рдЙрддреНрддрд░ рджреЗрддреЛ, рдЖрдгрд┐ SpyTransport(inner) calls рдкреБрдвреЗ рдкрд╛рдард╡рддреЛ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ path calls рдордзреНрдпреЗ рдЬреЛрдбрддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 exam/demo.py doubles
python3 - <<'EOF'
from unittest import mock
from exam.grades import AttendanceClient, StudentNotFound, AttendanceUnavailable
from exam.lab import StubTransport, FakeAttendance, SpyTransport
for status in (200, 404, 500, 503):
    c = AttendanceClient(StubTransport(status, {"days_present": 150, "days_total": 200}))
    try: print(f"stub says {status} тЖТ eligible {c.eligible('9')}")
    except (StudentNotFound, AttendanceUnavailable) as e: print(f"stub says {status} тЖТ {type(e).__name__}")
fake = FakeAttendance().add("1", 149, 200).add("2", 150, 200)
spy = SpyTransport(fake); c = AttendanceClient(spy)
print("149/200:", c.eligible("1"), "┬╖ 150/200:", c.eligible("2"), "┬╖ calls:", spy.calls)
m = mock.Mock(side_effect=[(503, {}), (200, {"days_present": 180, "days_total": 200})])
c = AttendanceClient(m)
for _ in range(2):
    try: print("mock тЖТ", c.attendance_percent("5"))
    except AttendanceUnavailable: print("mock тЖТ AttendanceUnavailable")
print("mock was called", m.call_count, "times with", m.call_args_list[0])
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

doubles рдЫрд╛рдкрддреЗ:

   stub  (canned answer)      тЖТ attendance 86.0% ┬╖ eligible True
   fake  (a small working one) тЖТ student 42: 86.0% ┬╖ student 7: 70.0% ┬╖ eligible False
   spy   (records calls)      тЖТ calls ['/attendance/42']
   mock  (unittest.mock)      тЖТ a 503 raises AttendanceUnavailable; assert_called_once_with('/attendance/42') holds
   fake-based test: eligible True тЖТ passes ┬╖ mock-based test тЖТ FAILS (the mock checked HOW, not WHAT)

рддреБрдордЪрд╛ snippet рдЫрд╛рдкрддреЛ:

stub says 200 тЖТ eligible True
stub says 404 тЖТ StudentNotFound
stub says 500 тЖТ AttendanceUnavailable
stub says 503 тЖТ AttendanceUnavailable
149/200: False ┬╖ 150/200: True ┬╖ calls: ['/attendance/1', '/attendance/2']
mock тЖТ AttendanceUnavailable
mock тЖТ 90.0
mock was called 2 times with call('/attendance/5')

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Stub рдореБрд│реЗ рддреБрдореНрд╣реА рдЪрд╛рд░ рдУрд│реАрдВрдд рдкреНрд░рддреНрдпреЗрдХ status code рдкрд╛рд╣реВ рд╢рдХрд▓рд╛рдд тАФ рдЦрд░реА service рд╣реЗ рдорд╛рдЧрдгреАрд╡рд░ рдХреНрд╡рдЪрд┐рддрдЪ рдХрд░реЗрд▓. Fake рд▓рд╛ рдЕрд╢реА рдХрдб рд╕рд╛рдкрдбрд▓реА рдЬреА stub рд▓рд╛ рд╕рд╛рдкрдбрд▓реА рдирд╕рддреА: 200 рдкреИрдХреА 149 рджрд┐рд╡рд╕ рдореНрд╣рдгрдЬреЗ 74.5%, рдЬреЗ eligible рдирд╛рд╣реА. side_effect рдпрд╛рджреАрдореБрд│реЗ mock рдПрдХрджрд╛ fail рдЭрд╛рд▓реЗ рдЖрдгрд┐ рдордЧ рдЙрддреНрддрд░ рджрд┐рд▓реЗ тАФ retry test рдЪрд╛ рд╣рд╛рдЪ рдЖрдХрд╛рд░. рдЖрдгрд┐ demo over-mocking рдЪреА рдХрд┐рдВрдордд рджрд╛рдЦрд╡рддреЛ: URL рдордзрд▓реНрдпрд╛ рдирд┐рд░реБрдкрджреНрд░рд╡реА рдмрджрд▓рд╛рдиреЗ mock-based test рдореЛрдбрд▓реА, рддрд░ рдЪреБрдХреАрдЪреНрдпрд╛ field рдирд╛рд╡рд╛рдВрдЪрд╛ stub рдЖрдирдВрджрд╛рдиреЗ pass рдЭрд╛рд▓рд╛ рдЖрдгрд┐ production рдордзреНрдпреЗ KeyError 'days_present' рдиреЗ crash рдЭрд╛рд▓рд╛ рдЕрд╕рддрд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ unittest.mock.patch test рдЪреНрдпрд╛ рдХрд╛рд▓рд╛рд╡рдзреАрдкреБрд░рддреЗ рдПрдХ рдирд╛рд╡ рдмрджрд▓рддреЗ, рдЖрдгрд┐ autospec=True рдореБрд│реЗ double рдЦрд░реЗ function рд╕реНрд╡реАрдХрд╛рд░реВ рд╢рдХрдгрд╛рд░ рдирд╛рд╣реА рдЕрд╕реЗ calls рдирд╛рдХрд╛рд░рддреЛ:

from unittest import mock
import exam.grades

def test_report_uses_attendance():
    with mock.patch.object(exam.grades.AttendanceClient, "attendance_percent",
                           autospec=True, return_value=80.0):
        assert exam.grades.AttendanceClient(None).eligible("42") is True

autospec signature рддрдкрд╛рд╕рддреЗ, рдЙрддреНрддрд░реЗ рдирд╛рд╣реА тАФ рдЦрд▒реНрдпрд╛ service рдиреЗ field рдЪреЗ рдирд╛рд╡ рдмрджрд▓рд▓реЗ рд╣реЗ рддреЗ рд╕рд╛рдВрдЧреВ рд╢рдХрдд рдирд╛рд╣реА. HTTP рд╕рд╛рдареА, record-and-replay рдЖрдгрд┐ stub-server рд╕рд╛рдзрдиреЗ рдорджрдд рдХрд░рддрд╛рдд: responses рдЖрдгрд┐ respx (Python), WireMock (Java, рд╕реНрд╡рддрдВрддреНрд░ server рдореНрд╣рдгреВрдирд╣реА рдЪрд╛рд▓рддреЗ), MSW (JavaScript). рддреБрдордЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ dependencies рд╕рд╛рдареА, рдЦрд▒реНрдпрд╛ implementation рд╢реЗрдЬрд╛рд░реА рддреБрдореНрд╣реА рд╕рд╛рдВрднрд╛рд│рд▓реЗрд▓рд╛ fake рд╡рд╛рдкрд░рд╛ рдЖрдгрд┐ рджреЛрдиреНрд╣реА рдПрдХрд╛рдЪ checks рд╡рд░ test рдХрд░рд╛.

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рддреБрдордЪреНрдпрд╛ suite рдордзрд▓реНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ double рдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░рд╛ "рдпрд╛рд▓рд╛ рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдХреЛрдг рдареЗрд╡рддреЗ?" рдЙрддреНрддрд░ "рдХреЛрдгреАрдЪ рдирд╛рд╣реА" рдЕрд╕реЗрд▓, рддрд░ рддреЛ рджреБрд╕рд▒реНрдпрд╛ team рдЪреНрдпрд╛ service рдмрджреНрджрд▓рдЪрд╛ рдЕрдВрджрд╛рдЬ рдЖрд╣реЗ. Contract tests (рдзрдбрд╛ 07) рдХрд┐рдВрд╡рд╛ integration tests рдЪрд╛ рдЫреЛрдЯрд╛ рд╕рдВрдЪ (рдзрдбрд╛ 06) рддреНрдпрд╛ рдЕрдВрджрд╛рдЬрд╛рд▓рд╛ рддрдкрд╛рд╕рдгреАрдд рдмрджрд▓рддрд╛рдд.

тПня╕П рдкреБрдвреЗ

рдзрдбрд╛ 04 рдордзрд▓рд╛ fake store рд╣рд╛ Python dict рд╣реЛрддрд╛. рдЦрд░рд╛ store SQL рдЖрд╣реЗ. Integration tests рдЦрд░рд╛ SQLite database рд╡рд╛рдкрд░рддрд╛рдд тАФ рдЖрдгрд┐ fake рд▓рд╛ рди рд╕рд╛рдкрдбрдгрд╛рд░рд╛ bug рд╢реЛрдзрддрд╛рдд.

git checkout lesson-06-integration

ЁЯзСтАНЁЯПл Lesson 05 тАФ Test doubles: stand-in examiners

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-fixtures ┬╖ Next: lesson-06-integration


ЁЯУж What's in this branch

Lessons 01тАУ04, plus test doubles: stand-ins for the parts a unit test should not use for real. AttendanceClient in exam/grades.py asks the attendance service (another server) how many days a pupil came. Tests replace that service with a stub, a fake, a spy or a mock тАФ and learn where over-mocking makes a test check the wrong thing. StubTransport, FakeAttendance and SpyTransport in exam/lab.py, unittest.mock from the standard library, and doubles() in exam/demo.py.

ЁЯзТ Explain like I'm 5

The real attendance examiner, Mrs Rao, sits in another building. Walking there for every question takes ages, and some days she is on leave. So Katrina uses stand-ins. ЁЯзСтАНЁЯПл

Then Dipika changes how the question is worded ("for term 1, please") without changing the answer. The fake still answers: test passes. тЬЕ The mock expected the old wording: test fails. тЭМ The mock was checking how the code asked, not what it concluded.

And one day the stub shows a card with the wrong heading. The real Mrs Rao never writes "present" тАФ she writes "days_present". The test passed; the real school breaks. ЁЯШм

ЁЯЧ║я╕П Diagram

flowchart LR
    c["ЁЯУЗ AttendanceClient<br/>eligible if at least 75%"] --> t{"transport"}
    t -->|"production"| real["ЁЯПв real attendance service"]
    t -->|"stub"| s["canned 172/200 тЖТ 86.0%"]
    t -->|"fake"| f["working dict<br/>42: 86.0% ┬╖ 7: 70.0%"]
    t -->|"spy"| sp["records calls<br/>/attendance/42"]
    t -->|"mock"| m["expects exact call<br/>fails on ?term=1"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/testing/lesson-diagrams.html#l05

тЭУ What

The names come from Gerard Meszaros, xUnit Test Patterns (2007); Martin Fowler's essay Mocks Aren't Stubs explains the difference well.

ЁЯдФ Why

Because a unit test must be fast and certain, and the real service is neither: it is on the network (lesson 01 of the distributed-systems school), it can be down, and it holds data you do not control. Doubles let you ask "what happens on a 503?" in a millisecond. But every double is a claim about the real thing. A claim nobody checks can be wrong тАФ lesson 07 checks it.

ЁЯФз How (in this repo)

AttendanceClient(transport) in exam/grades.py calls transport("/attendance/<id>") and expects (status, body). It raises StudentNotFound on 404, AttendanceUnavailable on 5xx, and otherwise computes days_present ├Ч 100 ├╖ days_total, rounded to one decimal. In exam/lab.py, StubTransport(status, body) always returns the same pair, FakeAttendance keeps a dict of pupils and answers 404 for unknown ones, and SpyTransport(inner) passes calls through and appends each path to calls.

ЁЯзк Try it

python3 exam/demo.py doubles
python3 - <<'EOF'
from unittest import mock
from exam.grades import AttendanceClient, StudentNotFound, AttendanceUnavailable
from exam.lab import StubTransport, FakeAttendance, SpyTransport
for status in (200, 404, 500, 503):
    c = AttendanceClient(StubTransport(status, {"days_present": 150, "days_total": 200}))
    try: print(f"stub says {status} тЖТ eligible {c.eligible('9')}")
    except (StudentNotFound, AttendanceUnavailable) as e: print(f"stub says {status} тЖТ {type(e).__name__}")
fake = FakeAttendance().add("1", 149, 200).add("2", 150, 200)
spy = SpyTransport(fake); c = AttendanceClient(spy)
print("149/200:", c.eligible("1"), "┬╖ 150/200:", c.eligible("2"), "┬╖ calls:", spy.calls)
m = mock.Mock(side_effect=[(503, {}), (200, {"days_present": 180, "days_total": 200})])
c = AttendanceClient(m)
for _ in range(2):
    try: print("mock тЖТ", c.attendance_percent("5"))
    except AttendanceUnavailable: print("mock тЖТ AttendanceUnavailable")
print("mock was called", m.call_count, "times with", m.call_args_list[0])
EOF

тЬЕ Verify тАФ what you should see

doubles prints:

   stub  (canned answer)      тЖТ attendance 86.0% ┬╖ eligible True
   fake  (a small working one) тЖТ student 42: 86.0% ┬╖ student 7: 70.0% ┬╖ eligible False
   spy   (records calls)      тЖТ calls ['/attendance/42']
   mock  (unittest.mock)      тЖТ a 503 raises AttendanceUnavailable; assert_called_once_with('/attendance/42') holds
   fake-based test: eligible True тЖТ passes ┬╖ mock-based test тЖТ FAILS (the mock checked HOW, not WHAT)

Your snippet prints:

stub says 200 тЖТ eligible True
stub says 404 тЖТ StudentNotFound
stub says 500 тЖТ AttendanceUnavailable
stub says 503 тЖТ AttendanceUnavailable
149/200: False ┬╖ 150/200: True ┬╖ calls: ['/attendance/1', '/attendance/2']
mock тЖТ AttendanceUnavailable
mock тЖТ 90.0
mock was called 2 times with call('/attendance/5')

ЁЯПБ What you just proved

A stub let you visit every status code in four lines тАФ something the real service would rarely do on demand. The fake found an edge a stub would not: 149 of 200 days is 74.5%, which is not eligible. The side_effect list made the mock fail once and then answer, the shape of a retry test. And the demo shows the price of over-mocking: a harmless change to the URL broke the mock-based test, while a stub with the wrong field names passed happily and would crash in production with KeyError 'days_present'.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ unittest.mock.patch swaps a name for the length of a test, and autospec=True makes the double refuse calls the real function could not accept:

from unittest import mock
import exam.grades

def test_report_uses_attendance():
    with mock.patch.object(exam.grades.AttendanceClient, "attendance_percent",
                           autospec=True, return_value=80.0):
        assert exam.grades.AttendanceClient(None).eligible("42") is True

autospec checks the signature, not the answers тАФ it cannot tell you the real service renamed a field. For HTTP, record-and-replay and stub-server tools help: responses and respx (Python), WireMock (Java, also runs as a standalone server), MSW (JavaScript). For your own dependencies, prefer a fake you maintain next to the real implementation and test both against the same checks.

ЁЯПн Why this matters in production: for each double in your suite, ask "what keeps this honest?" If the answer is "nothing", it is a guess about another team's service. Contract tests (lesson 07) or a small set of integration tests (lesson 06) turn the guess into a check.

тПня╕П Next

The fake store in lesson 04 was a Python dict. The real store is SQL. Integration tests use a real SQLite database тАФ and find a bug the fake cannot.

git checkout lesson-06-integration
тЖР PreviousfixturesNext тЖТintegration

This page is the lesson's README from the lesson-05-test-doubles branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.