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

ЁЯЧВя╕П рдзрдбрд╛ 04 тАФ Fixtures рдЖрдгрд┐ test data: рддрдпрд╛рд░ рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛

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


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

рдзрдбреЗ 01тАУ03, рдЖрдгрд┐ fixtures: test рд▓рд╛ рдЖрдкрд▓рд╛ рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рдгреНрдпрд╛рдкреВрд░реНрд╡реА рд▓рд╛рдЧрдгрд╛рд░рд╛ data рдЖрдгрд┐ setup. Builders рдПрдХрд╛ рдУрд│реАрдд рд╡реИрдз test data рдмрдирд╡рддрд╛рдд; setup рдЖрдгрд┐ teardown рдкреНрд░рддреНрдпреЗрдХ test рд▓рд╛ рдирд╡реА sheet рджреЗрддрд╛рдд рдЖрдгрд┐ рдирдВрддрд░ рд╕рд╛рдлрд╕рдлрд╛рдИ рдХрд░рддрд╛рдд, test crash рдЭрд╛рд▓реА рддрд░реАрд╣реА; рдЖрдгрд┐ рд╕реНрд╡рддрдВрддреНрд░ tests рдХреЛрдгрддреНрдпрд╛рд╣реА рдХреНрд░рдорд╛рдиреЗ pass рд╣реЛрддрд╛рдд. exam/lab.py рдордзрд▓реЗ StudentBuilder, a_student() рдЖрдгрд┐ Runner.run(setup=тАж, teardown=тАж); exam/demo.py рдордзрд▓реЗ fixtures().

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

рдкрд░реАрдХреНрд╖реЗрдкреВрд░реНрд╡реА office рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрд╕рд╛рдареА рдПрдХ рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛ рддрдпрд╛рд░ рдХрд░рддреЗ: рдирд╛рд╡ рдЫрд╛рдкрд▓реЗрд▓реЗ, рдЪреМрдХрдЯреА рддрдпрд╛рд░. ЁЯЧВя╕П рддреЛрдЪ fixture.

рдХрддрд░рд┐рдирд╛рдХрдбреЗ рдПрдХ рд╣реБрд╢рд╛рд░ рдпреБрдХреНрддреА рдЖрд╣реЗ: рдПрдХ рддрдпрд╛рд░ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА. a_student() рдореНрд╣рдгрдЬреЗ рд╕реНрд╡рддрдГ рдХрддрд░рд┐рдирд╛, рдЪрд╛рдВрдЧрд▓реЗ рдЧреБрдг рдЖрдгрд┐ 90% attendance рд╕рд╣ тАФ pass рд╣реЛрдгрд╛рд░реА рд╕рд╛рдорд╛рдиреНрдп рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА. "рдХрдореА attendance" test рдХрд░рдгреНрдпрд╛рд╕рд╛рдареА рддреА рдлрдХреНрдд рддреЗрд╡рдвреЗрдЪ рдмрджрд▓рддреЗ: a_student().with_attendance(60). рдмрд╛рдХреА рд╕рдЧрд│реЗ рд╕рд╛рдорд╛рдиреНрдп рд░рд╛рд╣рддреЗ, рдореНрд╣рдгреВрди test рд╕реНрдкрд╖реНрдЯрдкрдгреЗ attendance рдмрджреНрджрд▓рдЪ рдЖрд╣реЗ.

рдордЧ рдПрдХ рдЧреЛрдВрдзрд│. рджреЛрди рдкреНрд░рд╢реНрди рдПрдХрдЪ sheet рд╡рд╛рдкрд░рддрд╛рдд. рдкрд╣рд┐рд▓рд╛ рдкреНрд░рд╢реНрди рддреНрдпрд╛рд╡рд░ рдРрд╢реНрд╡рд░реНрдпрд╛рдЪреЗ рдирд╛рд╡ рд▓рд┐рд╣рд┐рддреЛ. рджреБрд╕рд▒реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рд▓рд╛ рдХреЛрд░реА sheet рдЕрдкреЗрдХреНрд╖рд┐рдд рдЕрд╕рддреЗ тАФ рдЖрдгрд┐ рддреНрдпрд╛рд▓рд╛ рдирд╛рд╡ рд╕рд╛рдкрдбрддреЗ. тЭМ рдХреНрд░рдо рдЙрд▓рдЯрд╛ рдХрд░рд╛ рдЖрдгрд┐ рджреЛрдиреНрд╣реА pass. тЬЕ рдЖрдзреА рдХреЛрдг рдЧреЗрд▓реЗ рдпрд╛рд╡рд░ pass рдХрд┐рдВрд╡рд╛ fail рд╣реЛрдгрд╛рд░реА test рд╣реА рд╡рд╛рдИрдЯ test рдЖрд╣реЗ.

рджреБрд░реБрд╕реНрддреА: рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рд╢реНрдирд╛рд╕рд╛рдареА рдирд╡реА sheet (setup), рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рд╢реНрдирд╛рдирдВрддрд░ sheet рдкрд░рдд рдШреЗрддрд▓реА рдЬрд╛рддреЗ (teardown), рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдиреЗ рд╢рд╛рдИ рд╕рд╛рдВрдбрд▓реА рддрд░реАрд╣реА. ЁЯз╣

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

flowchart LR
    b["ЁЯПЧя╕П a_student()<br/>Katrina ┬╖ avg 75.0 ┬╖ A ┬╖ promoted"] --> b1[".with_attendance(60)<br/>promoted False"]
    b1 --> b2[".with_medical_note()<br/>promoted True"]
    subgraph shared["ЁЯШм one shared sheet"]
      s1["enrol then empty: 1 passed, 1 failed"]
      s2["empty then enrol: 2 passed"]
    end
    subgraph fresh["тЬЕ fresh sheet per test"]
      f1["setup тЖТ test тЖТ teardown<br/>2 passed in both orders"]
    end

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдмрд╣реБрддреЗрдХ test suites рдордзрд▓рд╛ рдмрд╣реБрддрд╛рдВрд╢ code рдореНрд╣рдгрдЬреЗ test data рдЕрд╕рддреЛ. рдкреНрд░рддреНрдпреЗрдХ test 12 fields рд╕рд╣ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рд╣рд╛рддрд╛рдиреЗ рдмрдирд╡рдд рдЕрд╕реЗрд▓, рддрд░ рдПрдХ рдирд╡реЗ required field 300 tests рдореЛрдбрддреЗ, рдЖрдгрд┐ test рдХреЛрдгрддреНрдпрд╛ field рдмрджреНрджрд▓ рдЖрд╣реЗ рддреЗ рдХреЛрдгрд╛рд▓рд╛рдЪ рджрд┐рд╕рдд рдирд╛рд╣реА. рдЖрдгрд┐ shared state рдореБрд│реЗ рдХреНрд░рдорд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди failures рдпреЗрддрд╛рдд тАФ рддреНрдпрд╛ рддреБрдордЪреНрдпрд╛ laptop рд╡рд░ pass рд╣реЛрддрд╛рдд рдЖрдгрд┐ CI рдордзреНрдпреЗ fail, рдЬреЗ tests рд╡реЗрдЧрд│реНрдпрд╛ рдХреНрд░рдорд╛рдиреЗ рдХрд┐рдВрд╡рд╛ parallel рдордзреНрдпреЗ рдЪрд╛рд▓рд╡рддреЗ (рдзрдбрд╛ 11).

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

exam/lab.py рдордзрд▓рд╛ StudentBuilder рдХрддрд░рд┐рдирд╛рдкрд╛рд╕реВрди рд╕реБрд░реВ рд╣реЛрддреЛ (maths 80, science 70, english 75, attendance 90). named(), with_marks(**m), with_attendance() рдЖрдгрд┐ with_medical_note() рдкреНрд░рддреНрдпреЗрдХреА рдПрдХ рдЧреЛрд╖реНрдЯ рдмрджрд▓рддрд╛рдд рдЖрдгрд┐ builder рдкрд░рдд рджреЗрддрд╛рдд; card() рдЦрд░реЗ report_card() рдмреЛрд▓рд╡рддреЗ. Runner.run(setup=тАж, teardown=тАж) рдкреНрд░рддреНрдпреЗрдХ test рдЖрдзреА setup() рдмреЛрд▓рд╡рддреЛ, рддреНрдпрд╛рдЪрд╛ рдирд┐рдХрд╛рд▓ test рд▓рд╛ рджреЗрддреЛ, рдЖрдгрд┐ finally: block рдордзреНрдпреЗ teardown(fixture) рдмреЛрд▓рд╡рддреЛ.

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

python3 exam/demo.py fixtures
python3 - <<'EOF'
from exam.lab import a_student, Runner, expect, summary
print(a_student().card())
print(a_student().with_marks(maths=10).card()["grade"], a_student().with_marks(maths=10, science=10).card()["grade"])
log = []
def setup(): log.append("setup"); return {"store": [], "open": True}
def teardown(fx): fx["open"] = False; log.append("teardown")
def saves_one(fx): fx["store"].append("Katrina"); expect(len(fx["store"]), 1)
def crashes(fx): fx["store"].append("Dipika"); raise RuntimeError("printer jammed")
def starts_empty(fx): expect(fx["store"], [])
r = Runner(); r.tests = [saves_one, crashes, starts_empty]
for n, o in r.run(setup=setup, teardown=teardown): print(f"{n:<13} {o}")
print(log)
EOF

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

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

тФАтФА a_student() тАФ a prepared answer sheet with sensible defaults тЖТ Katrina ┬╖ average 75.0 ┬╖ A ┬╖ promoted True
   .with_attendance(60) тЖТ promoted False ┬╖ + .with_medical_note() тЖТ promoted True
   .named('Dipika').with_marks(maths=20, science=30, english=40) тЖТ average 30.0 ┬╖ F
тФАтФА one SHARED answer sheet: order enrolтЖТempty: 1 passed, 1 failed ┬╖ order emptyтЖТenrol: 2 passed, 0 failed
   a FRESH sheet per test (setup): 2 passed, 0 failed ┬╖ reversed: 2 passed, 0 failed ┬╖ teardown ran 2 times

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

{'name': 'Katrina', 'average': 75.0, 'grade': 'A', 'attendance': 90, 'promoted': True}
C F
saves_one     pass
crashes       ERROR тАФ RuntimeError: printer jammed
starts_empty  pass
['setup', 'teardown', 'setup', 'teardown', 'setup', 'teardown']

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

Builder рдиреЗ рдкреНрд░рддреНрдпреЗрдХ рдУрд│реАрдд рдПрдХрдЪ рдмрджрд▓ рдХреЗрд▓рд╛ рдЖрдгрд┐ рдмрд╛рдХреА рдХрд╛рд╣реАрдЪ рд╣рд▓рд▓реЗ рдирд╛рд╣реА: maths рдордзрд▓рд╛ рдПрдХрдЪ 10 рдХрддрд░рд┐рдирд╛рд▓рд╛ C рд╡рд░ рдЖрдгрддреЛ, рджреЛрди рддрд┐рд▓рд╛ F рд╡рд░ рдЖрдгрддрд╛рдд. Shared sheet рд╕рд╣, рдирд┐рдХрд╛рд▓ рдХреНрд░рдорд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рд╣реЛрддрд╛ (1 failed рд╡рд┐рд░реБрджреНрдз 0 failed). рдкреНрд░рддреНрдпреЗрдХ test рд▓рд╛ рдирд╡реА sheet рдЕрд╕рддрд╛рдирд╛, crashes рдиреЗ рджреАрдкрд┐рдХрд╛рдЪреЗ рдирд╛рд╡ рд▓рд┐рд╣реВрди рдЙрдбреА рдорд╛рд░рд▓реНрдпрд╛рдирдВрддрд░ рд▓рдЧреЗрдЪрд╣реА starts_empty pass рдЭрд╛рд▓реА тАФ рдЖрдгрд┐ log рдордзреНрдпреЗ рддреАрди setups рдЖрдгрд┐ рддреАрди teardowns рджрд┐рд╕рддрд╛рдд, рдореНрд╣рдгрдЬреЗ crash рдЭрд╛рд▓реЗрд▓реНрдпрд╛ test рдирдВрддрд░рд╣реА рд╕рд╛рдлрд╕рдлрд╛рдИ рдЭрд╛рд▓реА.

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

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

рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ yield рдирдВрддрд░ teardown рдЕрд╕рд▓реЗрд▓рд╛ pytest fixture; test fail рдЭрд╛рд▓реА рддрд░реА pytest yield рдирдВрддрд░рдЪрд╛ рднрд╛рдЧ рдЪрд╛рд▓рд╡рддреЗ:

import pytest
from exam.grades import GradeStore

@pytest.fixture
def store():
    s = GradeStore(":memory:")       # setup: a fresh database for THIS test
    yield s
    s.db.close()                     # teardown

def test_average(store):
    store.save("Katrina", "maths", 68)
    assert store.class_average("maths") == 68

рддреБрдореНрд╣рд╛рд▓рд╛ рднреЗрдЯрддреАрд▓ рдЕрд╕реЗ pytest рдЪреЗ built-in fixtures: tmp_path (рдкреНрд░рддреНрдпреЗрдХ test рд╕рд╛рдареА рдирд╡реА temporary directory), monkeypatch (environment variable рдХрд┐рдВрд╡рд╛ attribute set рдХрд░рд╛, test рдирдВрддрд░ рдкрд░рдд рдкреВрд░реНрд╡рд╡рдд), capsys (рдЫрд╛рдкрд▓реЗрд▓рд╛ output рдкрдХрдбрд╛). рд╣рд│реВ, read-only fixture рд▓рд╛ рдЬрд╛рд╕реНрдд рдореЛрдард╛ scope рджреЗрддрд╛ рдпреЗрддреЛ:

@pytest.fixture(scope="session")
def reference_marks():
    return load_big_csv("marks-2025.csv")    # loaded once, never modified

рдЗрддрд░ ecosystems: рд╡рд╛рд╕реНрддрд╡рд╡рд╛рджреА рдХрд┐рдорддреА рдЕрд╕рд▓реЗрд▓реНрдпрд╛ builders рд╕рд╛рдареА factory_boy (Python) рдЖрдгрд┐ Faker; JUnit 5 @BeforeEach / @AfterEach; Jest beforeEach / afterEach.

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдЕрдзреВрдирдордзреВрди рддреБрдордЪреА suite random рдХреНрд░рдорд╛рдиреЗ рдЪрд╛рд▓рд╡рд╛ (pytest-randomly plugin рд╣реЗ рдкреНрд░рддреНрдпреЗрдХ run рд▓рд╛ рдХрд░рддреЗ). рдлрдХреНрдд рдХрд╛рд╣реА рдХреНрд░рдорд╛рдВрдордзреНрдпреЗрдЪ fail рд╣реЛрдгрд╛рд░реА рдХреЛрдгрддреАрд╣реА test state рдЧрд│рд╡рдд рдЖрд╣реЗ тАФ CI рд░рд╛рддреНрд░реА 2 рд╡рд╛рдЬрддрд╛ рддреБрдордЪреНрдпрд╛рд╕рд╛рдареА рддреА рд╢реЛрдзрдгреНрдпрд╛рдкреВрд░реНрд╡реА рддреБрдореНрд╣реАрдЪ рд╢реЛрдзрд╛.

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

рдХрд╛рд╣реА рдЧреЛрд╖реНрдЯреА test рд▓рд╛ рдЦрд▒реНрдпрд╛ рд╕реНрд╡рд░реВрдкрд╛рдд рд╕рд╣рдЬ рд╡рд╛рдкрд░рддрд╛ рдпреЗрдд рдирд╛рд╣реАрдд: attendance service рджреБрд╕рд▒реНрдпрд╛ server рд╡рд░ рдЖрд╣реЗ. рдмрджрд▓реА рдкрд░реАрдХреНрд╖рдХ тАФ stubs, fakes, spies рдЖрдгрд┐ mocks тАФ рддрд┐рдЪреА рдЬрд╛рдЧрд╛ рдШреЗрддрд╛рдд.

git checkout lesson-05-test-doubles

ЁЯЧВя╕П Lesson 04 тАФ Fixtures & test data: prepared answer sheets

ЁЯУН You are here: Lesson 04 of 12 ┬╖ Previous: lesson-03-test-design ┬╖ Next: lesson-05-test-doubles


ЁЯУж What's in this branch

Lessons 01тАУ03, plus the fixtures: the data and setup a test needs before it can ask its question. Builders make valid test data with one line; setup and teardown give every test a fresh sheet and clean up after it, even when the test crashes; and independent tests pass in any order. StudentBuilder, a_student() and Runner.run(setup=тАж, teardown=тАж) in exam/lab.py; fixtures() in exam/demo.py.

ЁЯзТ Explain like I'm 5

Before an exam, the office prepares an answer sheet for every pupil: name printed, boxes ready. ЁЯЧВя╕П That is a fixture.

Katrina has a clever trick: a ready-made pupil. a_student() is Katrina herself, with good marks and 90% attendance тАФ a normal pupil who passes. To test "low attendance", she changes only that: a_student().with_attendance(60). Everything else stays normal, so the test is clearly about attendance.

Then a mess. Two questions share one sheet. The first question writes Aishwarya's name on it. The second question expects a blank sheet тАФ and finds a name. тЭМ Swap the order and both pass. тЬЕ A test that passes or fails depending on who went first is a bad test.

The fix: a fresh sheet for every question (setup), and the sheet is collected after each one (teardown), even if the pupil knocks the ink over. ЁЯз╣

ЁЯЧ║я╕П Diagram

flowchart LR
    b["ЁЯПЧя╕П a_student()<br/>Katrina ┬╖ avg 75.0 ┬╖ A ┬╖ promoted"] --> b1[".with_attendance(60)<br/>promoted False"]
    b1 --> b2[".with_medical_note()<br/>promoted True"]
    subgraph shared["ЁЯШм one shared sheet"]
      s1["enrol then empty: 1 passed, 1 failed"]
      s2["empty then enrol: 2 passed"]
    end
    subgraph fresh["тЬЕ fresh sheet per test"]
      f1["setup тЖТ test тЖТ teardown<br/>2 passed in both orders"]
    end

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

тЭУ What

ЁЯдФ Why

Because test data is most of the code in most test suites. When every test builds a pupil by hand with 12 fields, one new required field breaks 300 tests, and nobody can see which field a test is about. And shared state makes failures that depend on order тАФ they pass on your laptop and fail in CI, which runs tests in a different order or in parallel (lesson 11).

ЁЯФз How (in this repo)

StudentBuilder in exam/lab.py starts from Katrina (maths 80, science 70, english 75, attendance 90). named(), with_marks(**m), with_attendance() and with_medical_note() each change one thing and return the builder; card() calls the real report_card(). Runner.run(setup=тАж, teardown=тАж) calls setup() before each test, passes the result to it, and calls teardown(fixture) in a finally: block.

ЁЯзк Try it

python3 exam/demo.py fixtures
python3 - <<'EOF'
from exam.lab import a_student, Runner, expect, summary
print(a_student().card())
print(a_student().with_marks(maths=10).card()["grade"], a_student().with_marks(maths=10, science=10).card()["grade"])
log = []
def setup(): log.append("setup"); return {"store": [], "open": True}
def teardown(fx): fx["open"] = False; log.append("teardown")
def saves_one(fx): fx["store"].append("Katrina"); expect(len(fx["store"]), 1)
def crashes(fx): fx["store"].append("Dipika"); raise RuntimeError("printer jammed")
def starts_empty(fx): expect(fx["store"], [])
r = Runner(); r.tests = [saves_one, crashes, starts_empty]
for n, o in r.run(setup=setup, teardown=teardown): print(f"{n:<13} {o}")
print(log)
EOF

тЬЕ Verify тАФ what you should see

fixtures prints:

тФАтФА a_student() тАФ a prepared answer sheet with sensible defaults тЖТ Katrina ┬╖ average 75.0 ┬╖ A ┬╖ promoted True
   .with_attendance(60) тЖТ promoted False ┬╖ + .with_medical_note() тЖТ promoted True
   .named('Dipika').with_marks(maths=20, science=30, english=40) тЖТ average 30.0 ┬╖ F
тФАтФА one SHARED answer sheet: order enrolтЖТempty: 1 passed, 1 failed ┬╖ order emptyтЖТenrol: 2 passed, 0 failed
   a FRESH sheet per test (setup): 2 passed, 0 failed ┬╖ reversed: 2 passed, 0 failed ┬╖ teardown ran 2 times

Your snippet prints:

{'name': 'Katrina', 'average': 75.0, 'grade': 'A', 'attendance': 90, 'promoted': True}
C F
saves_one     pass
crashes       ERROR тАФ RuntimeError: printer jammed
starts_empty  pass
['setup', 'teardown', 'setup', 'teardown', 'setup', 'teardown']

ЁЯПБ What you just proved

The builder made one change per line and nothing else moved: a single 10 in maths drops Katrina to C, two drop her to F. With a shared sheet, the result depended on the order (1 failed vs 0 failed). With a fresh sheet per test, starts_empty passed even right after crashes wrote Dipika's name and blew up тАФ and the log shows three setups and three teardowns, so the crashed test was cleaned up too.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ a pytest fixture with teardown after yield; pytest runs the part after yield even when the test fails:

import pytest
from exam.grades import GradeStore

@pytest.fixture
def store():
    s = GradeStore(":memory:")       # setup: a fresh database for THIS test
    yield s
    s.db.close()                     # teardown

def test_average(store):
    store.save("Katrina", "maths", 68)
    assert store.class_average("maths") == 68

Built-in pytest fixtures you will meet: tmp_path (a fresh temporary directory per test), monkeypatch (set an environment variable or attribute, undone after the test), capsys (capture printed output). A slow, read-only fixture can have a wider scope:

@pytest.fixture(scope="session")
def reference_marks():
    return load_big_csv("marks-2025.csv")    # loaded once, never modified

Other ecosystems: factory_boy (Python) and Faker for builders with realistic values; JUnit 5 @BeforeEach / @AfterEach; Jest beforeEach / afterEach.

ЁЯПн Why this matters in production: run your suite in a random order now and then (the pytest-randomly plugin does it on every run). Any test that fails only in some orders is leaking state тАФ find it before CI does it for you at 2 a.m.

тПня╕П Next

Some things a test cannot easily use for real: the attendance service is on another server. Stand-in examiners тАФ stubs, fakes, spies and mocks тАФ take its place.

git checkout lesson-05-test-doubles
тЖР Previoustest designNext тЖТtest doubles

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