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

ЁЯТ╕ рдзрдбрд╛ 01 тАФ Test рдХрд╛ рдХрд░рд╛рдпрдЪреЗ: рдЙрд╢рд┐рд░рд╛ рд╕рд╛рдкрдбрд▓реЗрд▓реНрдпрд╛ bug рдЪреА рдХрд┐рдВрдордд

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 01 ┬╖ рдкреБрдвреЗ: lesson-02-unit-tests


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

рд╣рд╛ рд╕рдВрдкреВрд░реНрдг рдХреЛрд░реНрд╕ рдПрдХрд╛рдЪ рдХрд▓реНрдкрдиреЗрд╡рд░ рдЖрд╣реЗ: code рдореНрд╣рдгрдЬреЗ рдкрд░реАрдХреНрд╖реЗрд▓рд╛ рдмрд╕рд▓реЗрд▓реА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА, рдЖрдгрд┐ test рдореНрд╣рдгрдЬреЗ рдкрд░реАрдХреНрд╖реЗрддрд▓рд╛ рдкреНрд░рд╢реНрди, рдЬреНрдпрд╛рдЪреЗ рдмрд░реЛрдмрд░ рдЙрддреНрддрд░ рдЖрдзреАрдЪ рдорд╛рд╣реАрдд рдЕрд╕рддреЗ. рджреАрдкрд┐рдХрд╛рдиреЗ рд╢рд╛рд│реЗрдЪрд╛ grading code рд▓рд┐рд╣рд┐рд▓рд╛. рдореБрдЦреНрдп рдкрд░реАрдХреНрд╖рдХ рдХрддрд░рд┐рдирд╛рд▓рд╛ 1200 report cards рдЫрд╛рдкрдгреНрдпрд╛рдЪреНрдпрд╛ рдЖрдзреА рдХрд│рд╛рдпрд▓рд╛ рд╣рд╡реЗ рдХреА рддреЛ рдмрд░реЛрдмрд░ рдЖрд╣реЗ рдХрд╛. рд╕рдВрдкреВрд░реНрдг рдХреЛрд░реНрд╕рдордзреНрдпреЗ рддреБрдореНрд╣реА рд╡рд╛рдкрд░рдгрд╛рд░ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЦрд▒реНрдпрд╛ files:

ЁЯОТ рд╕реБрд░реВ рдХрд░рдгреНрдпрд╛рдкреВрд░реНрд╡реА: рддреБрдореНрд╣рд╛рд▓рд╛ рдлрдХреНрдд Python 3 рд▓рд╛рдЧреЗрд▓, рдмрд╛рдХреА рдХрд╛рд╣реА рдирд╛рд╣реА тАФ рдирд╛ pip install, рдирд╛ network. lab.py рдордзреНрдпреЗ рд╢рд┐рдХрд╡рдгреНрдпрд╛рд╕рд╛рдареАрдЪреЗ models рдЖрд╣реЗрдд, рдЦрд░реА рд╕рд╛рдзрдиреЗ рдирд╛рд╣реАрдд. Randomness рд▓рд╛ seed рджрд┐рд▓реЗрд▓рд╛ рдЖрд╣реЗ, рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ run рддреАрдЪ рдЧреЛрд╖реНрдЯ рдЫрд╛рдкрддреЗ. рдЦрд░реА рд╕рд╛рдзрдиреЗ рдореНрд╣рдгрдЬреЗ pytest, Hypothesis, Pact, Playwright, coverage.py рдЖрдгрд┐ mutmut; рдкреНрд░рддреНрдпреЗрдХ рдзрдбрд╛ рддреА рдЦрд▒реНрдпрд╛ project рд╡рд░ рдЕрд╢рд╛ рдЦреБрдгреЗрдЪреНрдпрд╛ blocks рдордзреНрдпреЗ рджрд╛рдЦрд╡рддреЛ, рдЬреНрдпрд╛рдВрдирд╛ pip install рд▓рд╛рдЧрддреЗ рдЖрдгрд┐ рдЬреЗ "рдХрд░реВрди рдкрд╛рд╣рд╛" рдЪрд╛ рднрд╛рдЧ рдирд╛рд╣реАрдд.

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

рд╢рд╛рд│реЗрдЪрд╛ рдирд┐рдпрдо рд╕реЛрдкрд╛ рдЖрд╣реЗ: рддреБрдордЪреА рдЯрдХреНрдХреЗрд╡рд╛рд░реА рдкреВрд░реНрдг рд╕рдВрдЦреНрдпреЗрдд round рдХреЗрд▓реА рдЬрд╛рддреЗ, рдЕрд░реНрдзрд╛ рд╡рд░ рдЬрд╛рддреЛ, рдЖрдгрд┐ 35 рдореНрд╣рдгрдЬреЗ pass.

рджреАрдкрд┐рдХрд╛ code рд▓рд┐рд╣рд┐рддреЗ. рдХрддрд░рд┐рдирд╛ рддреНрдпрд╛рд▓рд╛ рдкрд╛рдЪ рд╕реЛрдкреЗ рдкрд░реАрдХреНрд╖реЗрдЪреЗ рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рддреЗ: 90? A. 65? B. 50? C. 40? D. 10? F. рдкрд╛рдЪ рдкреИрдХреА рдкрд╛рдЪ. ЁЯОЙ

рдордЧ рд╕рдВрдкреВрд░реНрдг рд╢рд╛рд│рд╛ 200 рдЧреБрдгрд╛рдВрдЪрд╛ рдкреЗрдкрд░ рджреЗрддреЗ. 69 рдЧреБрдг рдорд┐рд│рд╛рд▓реЗрд▓реНрдпрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрд▓рд╛ 34.5% рдорд┐рд│рддрд╛рдд. рдирд┐рдпрдо рд╕рд╛рдВрдЧрддреЛ 34.5 тЖТ 35 тЖТ pass. рджреАрдкрд┐рдХрд╛рдЪрд╛ code рдореНрд╣рдгрддреЛ F. тЭМ

рдХрд╛? Python рдЪреЗ round() рдЕрд░реНрдзреНрдпрд╛рд▓рд╛ рд╡рд░ round рдХрд░рдд рдирд╛рд╣реА. рддреЗ рдЕрд░реНрдзреНрдпрд╛рд▓рд╛ рдЬрд╡рд│рдЪреНрдпрд╛ рд╕рдо рд╕рдВрдЦреНрдпреЗрдХрдбреЗ рдиреЗрддреЗ: round(34.5) рдореНрд╣рдгрдЬреЗ 34. рд╕реЛрдкреНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдиреА рдХрдзреА рдЕрд░реНрдзреНрдпрд╛рдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░рд▓реЗрдЪ рдирд╛рд╣реА.

рдЬрд░ рдХрддрд░рд┐рдирд╛рд▓рд╛ рд╣реЗ рджреАрдкрд┐рдХрд╛рдЪреНрдпрд╛ рдбреЗрд╕реНрдХрд╡рд░рдЪ рдЖрдгрдЦреА рдПрдХрд╛ рдкреНрд░рд╢реНрдирд╛рдиреЗ рд╕рд╛рдкрдбрд▓реЗ, рддрд░ рджреБрд░реБрд╕реНрддреА рдПрдХрд╛ рдУрд│реАрдЪреА рдЖрд╣реЗ. рдкрдг report cards рдкрд╛рдард╡рд▓реЗ рдЬрд╛рдИрдкрд░реНрдпрдВрдд рдХреЛрдгрд╛рд▓рд╛рдЪ рд╕рд╛рдкрдбрд▓реЗ рдирд╛рд╣реА, рддрд░ рддреНрдпрд╛рдЪреА рдХрд┐рдВрдордд 15 рдкреБрдирд░реНрдЫрдкрд╛рдИ, рдирд╛рдкрд╛рд╕ рд╕рд╛рдВрдЧрд┐рддрд▓реЗрд▓реНрдпрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрдирд╛ 5 рдорд╛рдлреАрдкрддреНрд░реЗ, рдЖрдгрд┐ рдПрдХ meeting. Bug рддреЛрдЪ. рдлрд░рдХ рдлрдХреНрдд рдПрдХрдЪ тАФ рддреЛ рдХрдзреА рд╕рд╛рдкрдбрд▓рд╛.

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

flowchart LR
    code["тЬНя╕П Dipika writes grade_v1<br/>round() sends halves to EVEN"]
    easy["ЁЯУЭ 5 easy questions<br/>90 65 50 40 10: 5/5 pass"]
    desk["ЁЯФН found at the desk<br/>1 failing check, 0 pupils hurt"]
    late["ЁЯУм found after posting<br/>15 wrong cards, 5 told they failed"]
    code --> easy
    easy -->|"one question at 34.5"| desk
    easy -->|"no such question"| late

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рджреБрд╕рд░рд╛ рдкрд░реНрдпрд╛рдп рдореНрд╣рдгрдЬреЗ software рд╡рд╛рдкрд░рдгрд╛рд▒реНрдпрд╛ рд▓реЛрдХрд╛рдВрдХрдбреВрди bugs рд╕рд╛рдкрдбрдгреЗ. рдпрд╛ рдХреЛрд░реНрд╕рдордзрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ test рд╣рд╛ рд▓рд╡рдХрд░ рд╡рд┐рдЪрд╛рд░рд▓реЗрд▓рд╛ рдкреНрд░рд╢реНрди рдЖрд╣реЗ тАФ machine рдХрдбреВрди, рдХрд╛рд╣реА рд╕реЗрдХрдВрджрд╛рдВрдд, рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдЬреЗрд╡реНрд╣рд╛ рдХреЛрдгреА code рдмрджрд▓рддреЛ. Tests рдореБрд│реЗ рддреБрдореНрд╣реА рди рдШрд╛рдмрд░рддрд╛ code рдмрджрд▓реВ рд╢рдХрддрд╛: рджреАрдкрд┐рдХрд╛ rounding рджреБрд░реБрд╕реНрдд рдХрд░рддреЗ рддреЗрд╡реНрд╣рд╛ 12 checks рддрд┐рд▓рд╛ рдПрдХрд╛ рд╕реЗрдХрдВрджрд╛рдд рд╕рд╛рдВрдЧрддрд╛рдд рдХреА рдмрд╛рдХреА рдХрд╛рд╣реА рдореЛрдбрд▓реЗ рдирд╛рд╣реА. рдЖрдгрд┐ test рдореНрд╣рдгрдЬреЗ рдЪрд╛рд▓рд╡рддрд╛ рдпреЗрдгрд╛рд░реЗ documentation: grade(34.5) == "D" рд╣рд╛ рдирд┐рдпрдо comment рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рд╕реНрдкрд╖реНрдЯ рд╕рд╛рдВрдЧрддреЗ.

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

exam/grades.py рдордзрд▓реЗ grade_v1(score) Python рдЪреЗ round() рд╡рд╛рдкрд░рддреЗ. grade(score) рд╣реЗ round_half_up() рд╡рд╛рдкрд░рддреЗ, рдЬреЗ Decimal(str(score)) рдЖрдгрд┐ ROUND_HALF_UP рдиреЗ round рдХрд░рддреЗ. exam/demo.py рдордзрд▓реЗ why() grade_v1 рд▓рд╛ рдкрд╛рдЪ рд╕реЛрдкреЗ рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рддреЗ, рдордЧ 200 рдЧреБрдгрд╛рдВрдЪреНрдпрд╛ рдкреЗрдкрд░рд╡рд░ 1200 рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрдЪреНрдпрд╛ seeded рд╢рд╛рд│реЗрдЪреЗ grading рдХрд░рддреЗ (рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ score рдкреВрд░реНрдг рдХрд┐рдВрд╡рд╛ рдЕрд░реНрдзрд╛ рдЕрд╕рддреЛ) рдЖрдгрд┐ рджреБрд░реБрд╕реНрдд grade() рдкреЗрдХреНрд╖рд╛ рдХрд┐рддреА cards рд╡реЗрдЧрд│реЗ рдпреЗрддрд╛рдд рддреЗ рдореЛрдЬрддреЗ.

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

python3 exam/demo.py why
python3 - <<'EOF'
from exam.grades import grade, grade_v1
print("round() on halves:", [round(x) for x in (0.5, 1.5, 2.5, 34.5, 35.5, 74.5)])
for out_of in (100, 200, 40, 80):
    scores = [m * 100 / out_of for m in range(out_of + 1)]
    wrong = [s for s in scores if grade_v1(s) != grade(s)]
    print(f"paper out of {out_of:>3}: {len(scores):>3} possible scores ┬╖ v1 wrong at {wrong}")
EOF
python3 exam/test_exam.py

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

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

тФАтФА Dipika's grade_v1 sits 5 easy exam questions: 5/5 correct тАФ and it still has a bug
   the whole school sits a paper out of 200 (1200 students): 15 report cards get the wrong grade
   wrong scores: [34.5, 44.5, 74.5] ┬╖ 5 students told they FAILED at 34.5

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

round() on halves: [0, 2, 2, 34, 36, 74]
paper out of 100: 101 possible scores ┬╖ v1 wrong at []
paper out of 200: 201 possible scores ┬╖ v1 wrong at [34.5, 44.5, 74.5]
paper out of  40:  41 possible scores ┬╖ v1 wrong at []
paper out of  80:  81 possible scores ┬╖ v1 wrong at []

Checks рд╢реЗрд╡рдЯреА 12/12 passed рдЫрд╛рдкрддрд╛рдд.

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

рддреЛрдЪ code 100, 40 рдХрд┐рдВрд╡рд╛ 80 рдЧреБрдгрд╛рдВрдЪреНрдпрд╛ рдкреЗрдкрд░рд╕рд╛рдареА рдЕрдЪреВрдХ рдЖрд╣реЗ, рдЖрдгрд┐ 200 рдЧреБрдгрд╛рдВрдЪреНрдпрд╛ рдкреЗрдкрд░рд╕рд╛рдареА рдЪреБрдХреАрдЪрд╛ рдЖрд╣реЗ. рдлрдХреНрдд рдкреВрд░реНрдг рд╕рдВрдЦреНрдпреЗрддрд▓реЗ scores рд╡рд╛рдкрд░рдгрд╛рд░рд╛ test suite рдХрд╛рдпрдо pass рд╣реЛрдд рд░рд╛рд╣рд┐рд▓рд╛ рдЕрд╕рддрд╛. Bug 201 рдкреИрдХреА рдиреЗрдордХреНрдпрд╛ рддреАрди inputs рд╡рд░ рд░рд╛рд╣рддреЛ тАФ 34.5, 44.5 рдЖрдгрд┐ 74.5, рдореНрд╣рдгрдЬреЗ grade рд░реЗрд╖реЗрдЪреНрдпрд╛ рдЕрдЧрджреА рдЦрд╛рд▓рдЪреЗ рдЕрд╕реЗ рдЕрд░реНрдзреЗ, рдЬреНрдпрд╛рдВрдЪрд╛ рд╕рдо рд╢реЗрдЬрд╛рд░реА рдЦрд╛рд▓реА рдЖрд╣реЗ (59.5 рд╣реЗ 60 рд╡рд░ round рд╣реЛрддреЗ, рдЬреЛ рд╕рдо рдЖрд╣реЗ, рдореНрд╣рдгреВрди рддреЗ рдпреЛрдЧрд╛рдпреЛрдЧрд╛рдиреЗ рдмрд░реЛрдмрд░ рдпреЗрддреЗ). рддреЗ рддреАрди рд╢реЛрдзрдгреЗ рд╣рд╛рдЪ рдзрдбрд╛ 03 рдЪрд╛ рд╡рд┐рд╖рдп рдЖрд╣реЗ.

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

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

рд╣рд╛рдЪ check pytest рдордзреНрдпреЗ, рд╕рд░реНрд╡рд╛рдзрд┐рдХ рд╡рд╛рдкрд░рд▓рд╛ рдЬрд╛рдгрд╛рд░рд╛ Python test runner. рдЦрд▒реНрдпрд╛ project рд╡рд░ (pip install pytest), test_grades.py рдореНрд╣рдгреВрди save рдХрд░рд╛ рдЖрдгрд┐ pytest -q рдЪрд╛рд▓рд╡рд╛:

from exam.grades import grade

def test_half_mark_rounds_up_to_a_pass():
    assert grade(34.5) == "D"

pytest рд╕рд╛рдзреЗ assert рдкреБрдиреНрд╣рд╛ рд▓рд┐рд╣рд┐рддреЗ, рдореНрд╣рдгреВрди failure рдордзреНрдпреЗ рджреЛрдиреНрд╣реА рдмрд╛рдЬреВ рджрд┐рд╕рддрд╛рдд:

E       AssertionError: assert 'F' == 'D'

Python рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ documentation рдпрд╛рдЪ рд╕рд╛рдкрд│реНрдпрд╛рдмрджреНрджрд▓ рдЗрд╢рд╛рд░рд╛ рджреЗрддреЗ: round(2.5) рдореНрд╣рдгрдЬреЗ 2, рдХрд╛рд░рдг round() round-half-to-even ("banker's rounding") рд╡рд╛рдкрд░рддреЗ, рдЖрдгрд┐ 2.675 рд╕рд╛рд░рдЦреЗ floats рдЕрд░реНрдзреНрдпрд╛рдЪреНрдпрд╛ рдереЛрдбреЗ рдЦрд╛рд▓реА рд╕рд╛рдард╡рд▓реЗ рдЬрд╛рддрд╛рдд. рдкреИрд╕реЗ рдХрд┐рдВрд╡рд╛ рдЧреБрдгрд╛рдВрд╕рд╛рдареА рдард░рд╛рд╡рд┐рдХ рдирд╛рд╡рд╛рдЪрд╛ рдирд┐рдпрдо рд╣рд╡рд╛ рдЕрд╕реЗрд▓, рддрд░ decimal рд╡рд╛рдкрд░рд╛:

from decimal import Decimal, ROUND_HALF_UP
Decimal("34.5").quantize(Decimal(1), rounding=ROUND_HALF_UP)   # Decimal('35')

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: business рдордзрд▓реА рд╡реНрдпрдХреНрддреА рдПрдХрд╛ рд╡рд╛рдХреНрдпрд╛рдд рд╕рд╛рдВрдЧреВ рд╢рдХреЗрд▓ рдЕрд╢рд╛ рдкреНрд░рддреНрдпреЗрдХ рдирд┐рдпрдорд╛рд╕рд╛рдареА ("35 рдореНрд╣рдгрдЬреЗ pass", "рдЕрд░реНрдзрд╛ рд╡рд░ round рд╣реЛрддреЛ", "тВ╣500 рд╡рд░реАрд▓ orders рдореЛрдлрдд ship рд╣реЛрддрд╛рдд"), рддреЗрдЪ рд╡рд╛рдХреНрдп code рдордзреНрдпреЗ рд╕рд╛рдВрдЧрдгрд╛рд░рд╛ test рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛ тАФ рдиреЗрдордХреНрдпрд╛ рдХрдбреЗрд╕рд╣.

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

рдПрдХ рдкреНрд░рд╢реНрди рдореНрд╣рдгрдЬреЗ рдПрдХ test. рддреЛ рдЪрд╛рдВрдЧрд▓рд╛ рдХрд╕рд╛ рд▓рд┐рд╣рд╛рдпрдЪрд╛, рдЖрдгрд┐ рдЕрдиреЗрдХ рдХрд╕реЗ рдЪрд╛рд▓рд╡рд╛рдпрдЪреЗ? Arrange, act, assert, рддреБрдордЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ runner рдордзреНрдпреЗ рдЖрдгрд┐ standard library рдЪреНрдпрд╛ unittest рдордзреНрдпреЗ.

git checkout lesson-02-unit-tests

ЁЯТ╕ Lesson 01 тАФ Why test: the cost of a bug found late

ЁЯУН You are here: Lesson 01 of 12 ┬╖ Next: lesson-02-unit-tests


ЁЯУж What's in this branch

The one idea this whole course is about: code is a pupil sitting an exam, and a test is an exam question with a known right answer. Dipika wrote the school's grading code. Katrina, the head examiner, wants to know if it is right before 1200 report cards are printed. Real files you will use all the way through:

ЁЯОТ Before you start: you need Python 3 and nothing else тАФ no pip install, no network. lab.py holds teaching models, not the real tools. Randomness is seeded, so every run prints the same story. The real tools are pytest, Hypothesis, Pact, Playwright, coverage.py and mutmut; every lesson shows them in blocks marked on a real project, which need pip install and are not part of Try it.

ЁЯзТ Explain like I'm 5

The school rule is simple: your percentage is rounded to a whole number, halves go up, and 35 is a pass.

Dipika writes the code. Katrina asks it five easy exam questions: 90? A. 65? B. 50? C. 40? D. 10? F. Five out of five. ЁЯОЙ

Then the whole school sits a paper out of 200. A pupil with 69 marks gets 34.5%. The rule says 34.5 тЖТ 35 тЖТ pass. Dipika's code says F. тЭМ

Why? Python's round() does not round halves up. It rounds them to the nearest even number: round(34.5) is 34. The easy questions never asked about a half.

If Katrina finds this with one more question at Dipika's desk, the fix takes one line. If nobody finds it until the report cards are posted, it costs 15 reprints, 5 apology letters to pupils told they failed, and a meeting. Same bug. The only difference is when it was found.

ЁЯЧ║я╕П Diagram

flowchart LR
    code["тЬНя╕П Dipika writes grade_v1<br/>round() sends halves to EVEN"]
    easy["ЁЯУЭ 5 easy questions<br/>90 65 50 40 10: 5/5 pass"]
    desk["ЁЯФН found at the desk<br/>1 failing check, 0 pupils hurt"]
    late["ЁЯУм found after posting<br/>15 wrong cards, 5 told they failed"]
    code --> easy
    easy -->|"one question at 34.5"| desk
    easy -->|"no such question"| late

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

тЭУ What

ЁЯдФ Why

Because the alternative is finding bugs through the people who use the software. Every test in this course is a question asked early, by a machine, in seconds, every time anyone changes the code. Tests also let you change code without fear: when Dipika fixes the rounding, 12 checks tell her in a second that nothing else broke. And a test is executable documentation: grade(34.5) == "D" says the rule more clearly than a comment.

ЁЯФз How (in this repo)

grade_v1(score) in exam/grades.py uses Python's round(). grade(score) uses round_half_up(), which rounds with Decimal(str(score)) and ROUND_HALF_UP. why() in exam/demo.py asks grade_v1 five easy questions, then grades a seeded school of 1200 pupils on a paper out of 200 (so every score is a whole or a half) and counts how many cards differ from the fixed grade().

ЁЯзк Try it

python3 exam/demo.py why
python3 - <<'EOF'
from exam.grades import grade, grade_v1
print("round() on halves:", [round(x) for x in (0.5, 1.5, 2.5, 34.5, 35.5, 74.5)])
for out_of in (100, 200, 40, 80):
    scores = [m * 100 / out_of for m in range(out_of + 1)]
    wrong = [s for s in scores if grade_v1(s) != grade(s)]
    print(f"paper out of {out_of:>3}: {len(scores):>3} possible scores ┬╖ v1 wrong at {wrong}")
EOF
python3 exam/test_exam.py

тЬЕ Verify тАФ what you should see

why prints:

тФАтФА Dipika's grade_v1 sits 5 easy exam questions: 5/5 correct тАФ and it still has a bug
   the whole school sits a paper out of 200 (1200 students): 15 report cards get the wrong grade
   wrong scores: [34.5, 44.5, 74.5] ┬╖ 5 students told they FAILED at 34.5

Your snippet prints:

round() on halves: [0, 2, 2, 34, 36, 74]
paper out of 100: 101 possible scores ┬╖ v1 wrong at []
paper out of 200: 201 possible scores ┬╖ v1 wrong at [34.5, 44.5, 74.5]
paper out of  40:  41 possible scores ┬╖ v1 wrong at []
paper out of  80:  81 possible scores ┬╖ v1 wrong at []

The checks end with 12/12 passed.

ЁЯПБ What you just proved

The same code is perfect for a paper out of 100, 40 or 80, and wrong for a paper out of 200. A test suite that only used whole-number scores would pass for ever. The bug lives at exactly three inputs out of 201 тАФ 34.5, 44.5 and 74.5, the halves just below a grade line where the even neighbour is below it (59.5 rounds to 60, which is even, so it happens to be right). Finding those three is what lesson 03 is about.

тЪая╕П Common mistakes

ЁЯПн In production

The same check in pytest, the most used Python test runner. On a real project (pip install pytest), save as test_grades.py and run pytest -q:

from exam.grades import grade

def test_half_mark_rounds_up_to_a_pass():
    assert grade(34.5) == "D"

pytest rewrites plain assert so a failure shows both sides:

E       AssertionError: assert 'F' == 'D'

Python's own documentation warns about this exact trap: round(2.5) is 2, because round() uses round-half-to-even ("banker's rounding"), and floats such as 2.675 are stored slightly below the half. When money or marks need a named rule, use decimal:

from decimal import Decimal, ROUND_HALF_UP
Decimal("34.5").quantize(Decimal(1), rounding=ROUND_HALF_UP)   # Decimal('35')

ЁЯПн Why this matters in production: for every rule a business person can state in one sentence ("35 is a pass", "halves round up", "orders over тВ╣500 ship free"), there should be a test that says the same sentence in code тАФ including the exact edge.

тПня╕П Next

One question is a test. How do you write one well, and run many? Arrange, act, assert, in a runner of your own and in the standard library's unittest.

git checkout lesson-02-unit-tests
тЖР Course homeall lessonsNext тЖТunit tests

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