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

ЁЯОп рдзрдбрд╛ 03 тАФ Test design: рдпреЛрдЧреНрдп рдкреНрд░рд╢реНрди 34.5 рд╡рд░рдЪрд╛ bug рд╢реЛрдзрддрд╛рдд

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


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

рдзрдбреЗ 01тАУ02, рдЖрдгрд┐ рдкреНрд░рд╢реНрди рдХрд╕реЗ рдирд┐рд╡рдбрд╛рдпрдЪреЗ: equivalence classes (рдЙрддреНрддрд░рд╛рдЪреНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рдХрд╛рд░рд╛рд╕рд╛рдареА рдПрдХ рдкреНрд░рд╢реНрди), boundary values (рдиреЗрдордХреЗ рдХрдбрд╛рдВрд╡рд░рдЪреЗ рдкреНрд░рд╢реНрди, рдЬрд┐рдереЗ bugs рд░рд╛рд╣рддрд╛рдд) рдЖрдгрд┐ decision tables (рдирд┐рдпрдорд╛рдВрдЪреНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ рдЬреЛрдбреАрд╕рд╛рдареА рдПрдХ рдкреНрд░рд╢реНрди). рдпрд╛рдВрдЪреНрдпрд╛ рдорджрддреАрдиреЗ рдХрддрд░рд┐рдирд╛рд▓рд╛ grade_v1 рдЪрд╛ 34.5 рд╡рд░рдЪрд╛ bug рд╕рд╛рдкрдбрддреЛ тАФ рдЖрдгрд┐ рдЖрдгрдЦреА рджреЛрди. exam/demo.py рдордзрд▓реЗ design().

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

рдХрддрд░рд┐рдирд╛ рдкреНрд░рддреНрдпреЗрдХ рд╢рдХреНрдп score рдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░реВ рд╢рдХрдд рдирд╛рд╣реА. рдореНрд╣рдгреВрди рддреА рдЧрдЯрд╛рдВрдордзреНрдпреЗ рд╡рд┐рдЪрд╛рд░ рдХрд░рддреЗ. ЁЯЧВя╕П

45 рддреЗ 59 рдордзрд▓реНрдпрд╛ рд╕рдЧрд│реНрдпрд╛ scores рдирд╛ C рдорд┐рд│рддреЛ. 50 рдХрд┐рдВрд╡рд╛ 52 рдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░рд▓реЗ рддрд░реА рддрд┐рд▓рд╛ рддреЗрдЪ рдХрд│рддреЗ. рдореНрд╣рдгреВрди рд╕реБрд░реБрд╡рд╛рддреАрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рдЧрдЯрд╛рддреВрди рдПрдХ рдкреНрд░рд╢реНрди рдкреБрд░реЗрд╕рд╛: рдПрдХ F, рдПрдХ D, рдПрдХ C, рдПрдХ B, рдПрдХ A тАФ рдЖрдгрд┐ рджреЛрди рд╡рд┐рдЪрд┐рддреНрд░ scores, тИТ0.1 рдЖрдгрд┐ 100.1, рдЬреЗ рдирд╛рдХрд╛рд░рд▓реЗ рдЧреЗрд▓реЗрдЪ рдкрд╛рд╣рд┐рдЬреЗрдд.

рд╕рд╛рдд рдкреНрд░рд╢реНрди. рд╕рд╛рддрд╣реА pass. Bug рдЕрдЬреВрдирд╣реА рддрд┐рдереЗрдЪ рдЖрд╣реЗ.

рдордЧ рддреА рд╡рд┐рдЪрд╛рд░ рдХрд░рддреЗ: "рдЪреБрдХрд╛ рдХреБрдареЗ рд╣реЛрддрд╛рдд? рдЧрдЯрд╛рдВрдордзрд▓реНрдпрд╛ рдХрдбрд╛рдВрд╡рд░." ЁЯЪз рдореНрд╣рдгреВрди рддреА 34.4, 34.5, 34.6 рдЖрдгрд┐ 35 рдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░рддреЗ. рдЖрдгрд┐ рддрд╕реЗрдЪ 45, 60 рдЖрдгрд┐ 75 рд╡рд░.

34.5 рд╡рд░ code рдореНрд╣рдгрддреЛ F. рдирд┐рдпрдо рдореНрд╣рдгрддреЛ D. рдкрдХрдбрд▓рд╛. ЁЯОп рдЖрдгрд┐ 44.5 рдЖрдгрд┐ 74.5 рд╡рд░рд╕реБрджреНрдзрд╛.

рд╢реЗрд╡рдЯреА, report card: "average pass рдЕрд╕реЗрд▓ рдЖрдгрд┐ attendance рдХрд┐рдорд╛рди 75% рдЕрд╕реЗрд▓ рддрд░ promoted тАФ рдХрд┐рдВрд╡рд╛ medical note рдЕрд╕реЗрд▓ рддрд░". рддреАрди рд╣реЛ/рдирд╛рд╣реА рдкреНрд░рд╢реНрдирд╛рдВрдЪреНрдпрд╛ 8 рдЬреЛрдбреНрдпрд╛ рд╣реЛрддрд╛рдд. рдХрддрд░рд┐рдирд╛ рд╕рдЧрд│реНрдпрд╛ 8 рдЪрд╛ рдПрдХ рддрдХреНрддрд╛ рдХрд╛рдврддреЗ, рдореНрд╣рдгрдЬреЗ рдХреЛрдгрддреАрд╣реА рдЬреЛрдбреА рд╡рд┐рд╕рд░рд▓реА рдЬрд╛рдд рдирд╛рд╣реА.

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

flowchart LR
    subgraph eq["ЁЯЧВя╕П equivalence classes: 7 questions, 0 bugs"]
      e["-0.1 refused ┬╖ 17 F ┬╖ 40 D ┬╖ 52 C ┬╖ 67 B ┬╖ 88 A ┬╖ 100.1 refused"]
    end
    subgraph bv["ЁЯЪз boundary values: 16 questions, 3 bugs"]
      b["34.4 F ┬╖ 34.5 v1 F, rule D тЖР BUG ┬╖ 35 D<br/>44.5 тЖР BUG ┬╖ 59.5 ok by luck ┬╖ 74.5 тЖР BUG"]
    end
    subgraph dt["ЁЯУЛ decision table: 3 conditions, 8 rules"]
      d["average passes? ┬╖ attendance at least 75%? ┬╖ medical note?"]
    end
    eq --> bv --> dt

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рд╢рдХреНрдп inputs рдЪреА рд╕рдВрдЦреНрдпрд╛ рдкреНрд░рдЪрдВрдб рдЖрд╣реЗ рдЖрдгрд┐ рддреБрдореНрд╣рд╛рд▓рд╛ рдкрд░рд╡рдбрдгрд╛рд▒реНрдпрд╛ tests рдЪреА рд╕рдВрдЦреНрдпрд╛ рд▓рд╣рд╛рди. Test design рдореНрд╣рдгрдЬреЗ рдЪреБрдХреАрдЪреЗ рдЙрддреНрддрд░ рдкрдХрдбрдгреНрдпрд╛рдЪреА рд╕рд░реНрд╡рд╛рдзрд┐рдХ рд╢рдХреНрдпрддрд╛ рдЕрд╕рд▓реЗрд▓реЗ рдереЛрдбреЗ рдкреНрд░рд╢реНрди рдирд┐рд╡рдбрдгреЗ. рд╕рд╛рдд class рдкреНрд░рд╢реНрдирд╛рдВрдЪреА рдХрд┐рдВрдордд рд╕рд╛рдд boundary рдкреНрд░рд╢реНрдирд╛рдВрдЗрддрдХреАрдЪ рдЖрд╣реЗ, рдкрдг bug рдлрдХреНрдд boundary рдкреНрд░рд╢реНрдирд╛рдВрдирд╛рдЪ рд╕рд╛рдкрдбрд▓рд╛.

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

exam/demo.py рдордзрд▓реЗ design() рд╕рд╛рдд class рдХрд┐рдорддреА grade рдордзреВрди рдЪрд╛рд▓рд╡рддреЗ, рдордЧ рджрд╣рд╛ boundary рдХрд┐рдорддреА grade_v1 рдЖрдгрд┐ grade рджреЛрдиреНрд╣реАрдВрдордзреВрди рдЪрд╛рд▓рд╡реВрди рдкреНрд░рддреНрдпреЗрдХ рдлрд░рдХ рдЦреБрдгрд╛рд╡рддреЗ, рдордЧ 0 рддреЗ 100 рдордзрд▓реЗ рд╕рдЧрд│реЗ 201 half marks. Decision table exam/grades.py рдордзрд▓реЗ report_card() average (70 рдХрд┐рдВрд╡рд╛ 30), attendance (90% рдХрд┐рдВрд╡рд╛ 60%) рдЖрдгрд┐ medical note (рд╣реЛ рдХрд┐рдВрд╡рд╛ рдирд╛рд╣реА) рдпрд╛рдВрдЪреНрдпрд╛ 8 рдкреИрдХреА рдкреНрд░рддреНрдпреЗрдХ рдЬреЛрдбреАрд╕рд╛рдареА рдмреЛрд▓рд╡рддреЗ.

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

python3 exam/demo.py design
python3 - <<'EOF'
from exam.grades import grade, grade_v1
classes = {"invalid low": -0.1, "F": 17, "D": 40, "C": 52, "B": 67, "A": 88, "invalid high": 100.1}
for name, s in classes.items():
    try: print(f"{name:<12} {s:>5} тЖТ v1 {grade_v1(s)} ┬╖ fixed {grade(s)}")
    except ValueError: print(f"{name:<12} {s:>5} тЖТ refused")
edges = []
for cut in (35, 45, 60, 75):
    edges += [cut - 0.6, cut - 0.5, cut - 0.4, cut]
found = [s for s in edges if grade_v1(s) != grade(s)]
print(f"{len(edges)} boundary questions, v1 wrong at {[round(s, 1) for s in found]}")
print("the classes alone found:", [n for n, s in classes.items() if s >= 0 and s <= 100 and grade_v1(s) != grade(s)])
EOF

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

design рдЫрд╛рдкрддреЗ (рдХрд╛рд╣реА рднрд╛рдЧ):

    34.4: v1 F ┬╖ fixed F
    34.5: v1 F ┬╖ fixed D   тЖР BUG
      35: v1 D ┬╖ fixed D
    44.5: v1 D ┬╖ fixed C   тЖР BUG
    59.5: v1 B ┬╖ fixed B
    74.5: v1 B ┬╖ fixed A   тЖР BUG
   every half-mark 0тАУ100 (201 scores): v1 is wrong at [34.5, 44.5, 74.5] тАФ round() sends halves to the EVEN number
   average 70 ┬╖ attendance 60% ┬╖ note yes тЖТ promoted
   average 30 ┬╖ attendance 90% ┬╖ note yes тЖТ held back

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

invalid low   -0.1 тЖТ refused
F               17 тЖТ v1 F ┬╖ fixed F
D               40 тЖТ v1 D ┬╖ fixed D
C               52 тЖТ v1 C ┬╖ fixed C
B               67 тЖТ v1 B ┬╖ fixed B
A               88 тЖТ v1 A ┬╖ fixed A
invalid high 100.1 тЖТ refused
16 boundary questions, v1 wrong at [34.5, 44.5, 74.5]
the classes alone found: []

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

рд╕рд╛рдд рдиреАрдЯ рдирд┐рд╡рдбрд▓реЗрд▓реНрдпрд╛ class рдкреНрд░рд╢реНрдирд╛рдВрдирд╛ рдХрд╛рд╣реАрдЪ рд╕рд╛рдкрдбрд▓реЗ рдирд╛рд╣реА; рд╕реЛрд│рд╛ boundary рдкреНрд░рд╢реНрдирд╛рдВрдирд╛ рддрд┐рдиреНрд╣реА рдЪреБрдХреАрдЪреЗ scores рд╕рд╛рдкрдбрд▓реЗ. Classes рддрд░реАрд╣реА рдЙрдкрдпреЛрдЧреА рдЖрд╣реЗрдд тАФ рдкреНрд░рддреНрдпреЗрдХ grade рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪрддрд╛ рдпреЗрддреЗ рдЖрдгрд┐ рд╡рд┐рдЪрд┐рддреНрд░ scores рдирд╛рдХрд╛рд░рд▓реЗ рдЬрд╛рддрд╛рдд рд╣реЗ рддреНрдпрд╛ рд╕рд┐рджреНрдз рдХрд░рддрд╛рдд тАФ рдкрдг bugs рдХрдбрд╛рдВрд╡рд░ рд╣реЛрддреЗ. Decision table рдЕрд╕рд╛ рдирд┐рдпрдо рджрд╛рдЦрд╡рддреЗ рдЬреЛ рд╡рд╛рдЪрдгрд╛рд▒реНрдпрд╛рдЪреНрдпрд╛ рдирдЬрд░реЗрддреВрди рд╕реБрдЯреВ рд╢рдХрддреЛ: medical note рдХрдореА attendance рд▓рд╛ рд╡рд╛рдЪрд╡рддреЗ, рдкрдг fail рд╣реЛрдгрд╛рд▒реНрдпрд╛ average рд▓рд╛ рдХрдзреАрдЪ рдирд╛рд╣реА (average 30 ┬╖ note yes тЖТ held back).

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

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

рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ boundary рддрдХреНрддрд╛ рдПрдХрд╛рдЪ parametrized pytest test рдордзреНрдпреЗ; рдкреНрд░рддреНрдпреЗрдХ рдУрд│ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ line рд╡рд░ report рд╣реЛрддреЗ:

import pytest
from exam.grades import grade

@pytest.mark.parametrize("score, expected", [
    (34.4, "F"), (34.5, "D"), (35, "D"),
    (44.4, "D"), (44.5, "C"), (45, "C"),
    (59.4, "C"), (59.5, "B"), (60, "B"),
    (74.4, "B"), (74.5, "A"), (75, "A"),
])
def test_grade_boundaries(score, expected):
    assert grade(score) == expected

unittest рдордзреНрдпреЗ рд╣реАрдЪ рдХрд▓реНрдкрдирд╛ subTest рд╡рд╛рдкрд░рддреЗ, рдореНрд╣рдгрдЬреЗ рдПрдХ failing рдУрд│ рдмрд╛рдХреАрдЪреНрдпрд╛рдВрдирд╛ рд▓рдкрд╡рдд рдирд╛рд╣реА:

def test_boundaries(self):
    for score, expected in [(34.5, "D"), (44.5, "C"), (74.5, "A")]:
        with self.subTest(score=score):
            self.assertEqual(grade(score), expected)

рд╣реА рддрдВрддреНрд░реЗ ISTQB tester syllabus рдордзреНрдпреЗ standard рдЖрд╣реЗрдд (equivalence partitioning, boundary value analysis, decision table testing, state transition testing) рдЖрдгрд┐ рдХреЛрдгрддреНрдпрд╛рд╣реА рднрд╛рд╖реЗрдд рд▓рд╛рдЧреВ рд╣реЛрддрд╛рдд.

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдЬреНрдпрд╛ рдирд┐рдпрдорд╛рдд рдПрдЦрд╛рджрд╛ рдЖрдХрдбрд╛ рдЖрд╣реЗ тАФ pass mark, price threshold, rate limit, date cut-off тАФ рддреНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА рдЖрдзреА boundary рддрдХреНрддрд╛ рд▓рд┐рд╣рд╛. Users рдиреА report рдХреЗрд▓реЗрд▓реЗ рдмрд╣реБрддреЗрдХ "рдЕрд╢рдХреНрдп" bugs рдиреЗрдордХреЗ рдПрдЦрд╛рджреНрдпрд╛ рдХрдбреЗрд╡рд░ рдмрд╕рд▓реЗрд▓реЗ рдЕрд╕рддрд╛рдд.

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

рдЪрд╛рдВрдЧрд▓реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдирд╛ рдЪрд╛рдВрдЧрд▓реНрдпрд╛ рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛ рд▓рд╛рдЧрддрд╛рдд: test data. рдкреНрд░рддреНрдпреЗрдХ test рдордзреНрдпреЗ рддреНрдпрд╛ рд╣рд╛рддрд╛рдиреЗ рдмрдирд╡рдгреЗ рд╣рд│реВ рдЖрдгрд┐ рдирд╛рдЬреВрдХ рдЖрд╣реЗ. Builders, setup рдЖрдгрд┐ teardown, рдЖрдгрд┐ рдПрдХрдореЗрдХрд╛рдВрд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдирд╕рд▓реЗрд▓реЗ tests.

git checkout lesson-04-fixtures

ЁЯОп Lesson 03 тАФ Test design: the right questions find the bug at 34.5

ЁЯУН You are here: Lesson 03 of 12 ┬╖ Previous: lesson-02-unit-tests ┬╖ Next: lesson-04-fixtures


ЁЯУж What's in this branch

Lessons 01тАУ02, plus how to choose questions: equivalence classes (one question per kind of answer), boundary values (questions right at the edges, where bugs live) and decision tables (one question per combination of rules). With them, Katrina finds grade_v1's bug at 34.5 тАФ and two more. design() in exam/demo.py.

ЁЯзТ Explain like I'm 5

Katrina cannot ask about every possible score. So she thinks in groups. ЁЯЧВя╕П

All scores from 45 to 59 get a C. Asking about 50 or 52 tells her the same thing. So one question per group is enough to start: one F, one D, one C, one B, one A тАФ and two silly scores, тИТ0.1 and 100.1, which must be refused.

Seven questions. All seven pass. The bug is still there.

Then she thinks: "Where do mistakes happen? At the edges between groups." ЁЯЪз So she asks about 34.4, 34.5, 34.6 and 35. And the same at 45, 60 and 75.

At 34.5, the code says F. The rule says D. Caught. ЁЯОп And at 44.5 and 74.5 too.

Last, the report card: "promoted if the average passes and attendance is at least 75% тАФ or there is a medical note". Three yes/no questions make 8 combinations. Katrina draws a table with all 8, so no combination is forgotten.

ЁЯЧ║я╕П Diagram

flowchart LR
    subgraph eq["ЁЯЧВя╕П equivalence classes: 7 questions, 0 bugs"]
      e["-0.1 refused ┬╖ 17 F ┬╖ 40 D ┬╖ 52 C ┬╖ 67 B ┬╖ 88 A ┬╖ 100.1 refused"]
    end
    subgraph bv["ЁЯЪз boundary values: 16 questions, 3 bugs"]
      b["34.4 F ┬╖ 34.5 v1 F, rule D тЖР BUG ┬╖ 35 D<br/>44.5 тЖР BUG ┬╖ 59.5 ok by luck ┬╖ 74.5 тЖР BUG"]
    end
    subgraph dt["ЁЯУЛ decision table: 3 conditions, 8 rules"]
      d["average passes? ┬╖ attendance at least 75%? ┬╖ medical note?"]
    end
    eq --> bv --> dt

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

тЭУ What

ЁЯдФ Why

Because the number of possible inputs is enormous and the number of tests you can afford is small. Test design is how you pick the few questions most likely to catch a wrong answer. Seven class questions cost the same as seven boundary questions, but only the boundary questions found the bug.

ЁЯФз How (in this repo)

design() in exam/demo.py runs seven class values through grade, then ten boundary values through both grade_v1 and grade and marks each difference, then all 201 half marks from 0 to 100. The decision table calls report_card() from exam/grades.py for each of the 8 combinations of average (70 or 30), attendance (90% or 60%) and a medical note (yes or no).

ЁЯзк Try it

python3 exam/demo.py design
python3 - <<'EOF'
from exam.grades import grade, grade_v1
classes = {"invalid low": -0.1, "F": 17, "D": 40, "C": 52, "B": 67, "A": 88, "invalid high": 100.1}
for name, s in classes.items():
    try: print(f"{name:<12} {s:>5} тЖТ v1 {grade_v1(s)} ┬╖ fixed {grade(s)}")
    except ValueError: print(f"{name:<12} {s:>5} тЖТ refused")
edges = []
for cut in (35, 45, 60, 75):
    edges += [cut - 0.6, cut - 0.5, cut - 0.4, cut]
found = [s for s in edges if grade_v1(s) != grade(s)]
print(f"{len(edges)} boundary questions, v1 wrong at {[round(s, 1) for s in found]}")
print("the classes alone found:", [n for n, s in classes.items() if s >= 0 and s <= 100 and grade_v1(s) != grade(s)])
EOF

тЬЕ Verify тАФ what you should see

design prints (in part):

    34.4: v1 F ┬╖ fixed F
    34.5: v1 F ┬╖ fixed D   тЖР BUG
      35: v1 D ┬╖ fixed D
    44.5: v1 D ┬╖ fixed C   тЖР BUG
    59.5: v1 B ┬╖ fixed B
    74.5: v1 B ┬╖ fixed A   тЖР BUG
   every half-mark 0тАУ100 (201 scores): v1 is wrong at [34.5, 44.5, 74.5] тАФ round() sends halves to the EVEN number
   average 70 ┬╖ attendance 60% ┬╖ note yes тЖТ promoted
   average 30 ┬╖ attendance 90% ┬╖ note yes тЖТ held back

Your snippet prints:

invalid low   -0.1 тЖТ refused
F               17 тЖТ v1 F ┬╖ fixed F
D               40 тЖТ v1 D ┬╖ fixed D
C               52 тЖТ v1 C ┬╖ fixed C
B               67 тЖТ v1 B ┬╖ fixed B
A               88 тЖТ v1 A ┬╖ fixed A
invalid high 100.1 тЖТ refused
16 boundary questions, v1 wrong at [34.5, 44.5, 74.5]
the classes alone found: []

ЁЯПБ What you just proved

Seven well-chosen class questions found nothing; sixteen boundary questions found all three wrong scores. The classes are still worth having тАФ they prove each grade is reachable and that silly scores are refused тАФ but the bugs were at the edges. The decision table shows a rule a reader might miss: a medical note rescues low attendance, never a failing average (average 30 ┬╖ note yes тЖТ held back).

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ the boundary table as one parametrized pytest test; each row is reported on its own line:

import pytest
from exam.grades import grade

@pytest.mark.parametrize("score, expected", [
    (34.4, "F"), (34.5, "D"), (35, "D"),
    (44.4, "D"), (44.5, "C"), (45, "C"),
    (59.4, "C"), (59.5, "B"), (60, "B"),
    (74.4, "B"), (74.5, "A"), (75, "A"),
])
def test_grade_boundaries(score, expected):
    assert grade(score) == expected

The same idea in unittest uses subTest, so one failing row does not hide the others:

def test_boundaries(self):
    for score, expected in [(34.5, "D"), (44.5, "C"), (74.5, "A")]:
        with self.subTest(score=score):
            self.assertEqual(grade(score), expected)

These techniques are standard in the ISTQB tester syllabus (equivalence partitioning, boundary value analysis, decision table testing, state transition testing) and apply in any language.

ЁЯПн Why this matters in production: for every rule with a number in it тАФ a pass mark, a price threshold, a rate limit, a date cut-off тАФ write the boundary table first. Most "impossible" bugs reported by users sit exactly on an edge.

тПня╕П Next

Good questions need good answer sheets: the test data. Building them by hand in every test is slow and fragile. Builders, setup and teardown, and tests that do not depend on each other.

git checkout lesson-04-fixtures
тЖР Previousunit testsNext тЖТfixtures

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