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

ЁЯХ░я╕П рдзрдбрд╛ 11 тАФ Flaky tests: рднрд┐рдВрддреАрд╡рд░рдЪреЗ рдбреБрдЧрдбреБрдЧрдгрд╛рд░реЗ рдШрдбреНрдпрд╛рд│

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдорд╛рдЧреАрд▓: lesson-10-coverage-mutation ┬╖ рдкреБрдвреАрд▓: lesson-12-ci-tdd


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

рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп flaky tests: рддреНрдпрд╛рдЪ code рд╡рд░ рдХрдзреА pass рддрд░ рдХрдзреА fail рд╣реЛрдгрд╛рд▒реНрдпрд╛ tests. рдЪрд╛рд░ рдиреЗрд╣рдореАрдЪреА рдХрд╛рд░рдгреЗ тАФ рд╡реЗрд│, рдХреНрд░рдо (shared state), randomness рдЖрдгрд┐ рд╡рд╛рдЯ рдкрд╛рд╣рдгреЗ тАФ рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рд░рдг рдХрд╛рд╣реАрд╣реА рди рдмрджрд▓рддрд╛ test 1000 рд╡реЗрд│рд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡реВрди рджрд╛рдЦрд╡рд▓реЗрд▓реЗ, рдЖрдгрд┐ рдордЧ рдмрджрд▓рдгрд╛рд░реА рдЧреЛрд╖реНрдЯ рдкрдХреНрдХреА рдХрд░реВрди рд╕реБрдзрд╛рд░рд▓реЗрд▓реЗ. flaky_time, fixed_time, order_runs, flaky_random, flaky_wait рдЖрдгрд┐ rerun exam/lab.py рдордзреНрдпреЗ; is_late() exam/grades.py рдордзреНрдпреЗ; flaky() exam/demo.py рдордзреНрдпреЗ.

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

рдкрд░реАрдХреНрд╖рд╛ рд╣реЙрд▓рдордзреНрдпреЗ рднрд┐рдВрддреАрд╡рд░ рдПрдХ рдШрдбреНрдпрд╛рд│ рдЖрд╣реЗ рдЬреЗ рдбреБрдЧрдбреБрдЧрддреЗ. ЁЯХ░я╕П рдмрд╣реБрддреЗрдХ рджрд┐рд╡рд╕ рддреЗ рдмрд░реЛрдмрд░ рдЕрд╕рддреЗ. рдХрд╛рд╣реА рджрд┐рд╡рд╕ рддреЗ рдЙрдбреА рдорд╛рд░рддреЗ. рд▓рд┐рд╣рд┐рдгреЗ рдХрдзреА рдерд╛рдВрдмрд╡рд╛рдпрдЪреЗ рд╣реЗ рдард░рд╡рдгреНрдпрд╛рд╕рд╛рдареА рдШрдбреНрдпрд╛рд│рд╛рдХрдбреЗ рдкрд╛рд╣рдгрд╛рд▒реНрдпрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдЪреЗ рдХрдзреА рдХрдзреА рдЪреБрдХрддреЗ тАФ рддрд┐рдЪреНрдпрд╛ рдЙрддреНрддрд░рд╛рдВрдореБрд│реЗ рдирд╡реНрд╣реЗ, рддрд░ рдШрдбреНрдпрд╛рд│рд╛рдореБрд│реЗ.

Flaky test рдЕрдЧрджреА рддрд╕рд╛рдЪ рдЕрд╕рддреЛ. Code рдмрджрд▓рд▓рд╛ рдирд╛рд╣реА, рдкрдг test рдЪрд╛ рдирд┐рдХрд╛рд▓ рдмрджрд▓рд▓рд╛.

рдкрд░реНрдпрд╡реЗрдХреНрд╖рдг рдХрд░рдгрд╛рд▒реНрдпрд╛ рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ рдЪрд╛рд░ рдбреБрдЧрдбреБрдЧрдгрд╛рд░реА рдШрдбреНрдпрд╛рд│реЗ рд╕рд╛рдкрдбрддрд╛рдд:

  1. рд╡реЗрд│ тП░ тАФ рдПрдХ рдкреНрд░рд╢реНрди рддрдкрд╛рд╕рддреЛ "рддрд╛рд╕рд╛рднрд░рд╛рдд рджрд┐рд▓реЗрд▓рд╛ рдЧреГрд╣рдкрд╛рда рдЙрд╢рд┐рд░рд╛ рдирд╛рд╣реА". рддреЛ рджрд┐рд╡рд╕рднрд░ pass рд╣реЛрддреЛтАж рдЖрдгрд┐ 23:00 рдирдВрддрд░ fail рд╣реЛрддреЛ, рдЬреЗрд╡реНрд╣рд╛ "рддрд╛рд╕рд╛рднрд░рд╛рдд" рдореНрд╣рдгрдЬреЗ рдЙрджреНрдпрд╛ рдЕрд╕рддреЗ.
  2. рдХреНрд░рдо ЁЯФА тАФ рддреАрди рдкреНрд░рд╢реНрди рдПрдХрдЪ рд╡рд░реНрдЧ-рдпрд╛рджреА рд╡рд╛рдкрд░рддрд╛рдд. рдПрдХрд╛ рдХреНрд░рдорд╛рдиреЗ рдЪрд╛рд▓рд╡рд▓реЗ рддрд░ рд╕рдЧрд│реЗ pass. рдЗрддрд░ 5 рдкреИрдХреА рдХреЛрдгрддреНрдпрд╛рд╣реА рдХреНрд░рдорд╛рдиреЗ рдЪрд╛рд▓рд╡рд▓реЗ тАФ рдХрд╛рд╣реАрддрд░реА fail рд╣реЛрддреЗ.
  3. Randomness ЁЯО▓ тАФ рдПрдХ рдкреНрд░рд╢реНрди 3 random рдЧреБрдг рдирд┐рд╡рдбрддреЛ рдЖрдгрд┐ рдХрд┐рдорд╛рди рдПрдХ pass рдЕрдкреЗрдХреНрд╖рд┐рдд рдзрд░рддреЛ. рд╕рд╣рд╕рд╛ рдЦрд░реЗ. рдХрдзреА рдХрдзреА рддрд┐рдиреНрд╣реА 35 рдЪреНрдпрд╛ рдЦрд╛рд▓реА рдЕрд╕рддрд╛рдд.
  4. рд╡рд╛рдЯ рдкрд╛рд╣рдгреЗ тП│ тАФ рдПрдХ рдкреНрд░рд╢реНрди report card рдЫрд╛рдкрд╛рдпрд▓рд╛ рд╕реБрд░реВ рдХрд░рддреЛ, 200 ms рдерд╛рдВрдмрддреЛ, рдордЧ рддреЗ рдЭрд╛рд▓реЗ рдХрд╛ рддрдкрд╛рд╕рддреЛ. рд╕рд╣рд╕рд╛ рдЭрд╛рд▓реЗрд▓реЗ рдЕрд╕рддреЗ. рдХрдзреА рдХрдзреА printer рд╣рд│реВ рдЕрд╕рддреЛ.

рдРрд╢реНрд╡рд░реНрдпрд╛рдЪреА рдкрджреНрдзрдд: рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рд╢реНрди рдХрд╛рд╣реАрд╣реА рди рдмрджрд▓рддрд╛ 1000 рд╡реЗрд│рд╛ рдЪрд╛рд▓рд╡рд╛. рдЬреЛ рдкреНрд░рд╢реНрди рдХрдзреА pass рдЖрдгрд┐ рдХрдзреА fail рд╣реЛрддреЛ рддреЛ flaky. рдордЧ рддреА рдХрд╛рдп рдмрджрд▓рддреЗ рддреЗ рд╢реЛрдзрддреЗ тАФ рдЖрдгрд┐ рддреЗ рдкрдХреНрдХреЗ рдХрд░рддреЗ: рдард░рд▓реЗрд▓реЗ рдШрдбреНрдпрд╛рд│, рдирд╡реА рдпрд╛рджреА, рдард░рд▓реЗрд▓рд╛ seed, "рдкреВрд░реНрдг рд╣реЛрдИрдкрд░реНрдпрдВрдд рд╡рд╛рдЯ рдкрд╛рд╣рд╛".

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

flowchart LR
    r["ЁЯФБ rerun 1000 times<br/>no code change"] --> t["тП░ time: 33 failed<br/>after 23:00"]
    r --> o["ЁЯФА order: 5 of 6 orders fail"]
    r --> d["ЁЯО▓ randomness: 36 failed"]
    r --> w["тП│ sleep 200 ms: 76 failed"]
    t --> f1["inject a fixed clock тЖТ 0"]
    o --> f2["fresh fixture per test тЖТ 0"]
    d --> f3["fixed seed or fixed data"]
    w --> f4["wait for done, not for time"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг flaky suite рд▓реЛрдХрд╛рдВрдирд╛ рд▓рд╛рд▓ рд░рдВрдЧрд╛рдХрдбреЗ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░рд╛рдпрд▓рд╛ рд╢рд┐рдХрд╡рддреЛ. рдПрдХрджрд╛ "рдлрдХреНрдд рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡" рд╣реЗ рдиреЗрд╣рдореАрдЪреЗ рдЙрддреНрддрд░ рдЭрд╛рд▓реЗ, рдХреА рдЦрд░реЗ failure рд╕реБрджреНрдзрд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рд▓реЗ рдЬрд╛рддреЗ тАФ рдЖрдгрд┐ ship рд╣реЛрддреЗ. Flakiness рд╡реЗрд│рд╣реА рд╡рд╛рдпрд╛ рдШрд╛рд▓рд╡рддреЗ: рдзрдбрд╛ 08 рдЪреНрдпрд╛ model рдордзреНрдпреЗ 200 e2e tests рдЕрд╕рд▓реЗрд▓рд╛ suite 87% runs рдордзреНрдпреЗ рд╡рд┐рдирд╛рдХрд╛рд░рдг рд▓рд╛рд▓ рд╣реЛрддрд╛. рдЖрдгрд┐ flaky test рд╣реЗ рдХрдзреА рдХрдзреА flaky product рдЪреЗ рдПрдХрдореЗрд╡ рд▓рдХреНрд╖рдг рдЕрд╕рддреЗ тАФ рдЦрд░реА race condition, рдЦрд░рд╛ time-zone bug тАФ рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ flake рдХрдбреЗ рдПрдХрджрд╛ рдкрд╛рд╣рд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗ.

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

exam/lab.py рдордзреАрд▓ рдкреНрд░рддреНрдпреЗрдХ flaky test рдмрджрд▓рдгрд╛рд░реА рдЧреЛрд╖реНрдЯ parameter рдореНрд╣рдгреВрди рдШреЗрддреЗ, рдореНрд╣рдгрдЬреЗ рддреА рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддрд╛ рдпреЗрддреЗ. flaky_time(clock) due date рд╕рд╛рдареА clock() рдмреЛрд▓рд╡рддреЗ рдЖрдгрд┐ рдкреБрдиреНрд╣рд╛ hand-in рд╡реЗрд│ рдЕрдзрд┐рдХ рдПрдХ рддрд╛рд╕ рдпрд╛рд╕рд╛рдареА; exam/grades.py рдордзреАрд▓ is_late() рдореНрд╣рдгрддреЗ рдЙрд╢реАрд░ рдореНрд╣рдгрдЬреЗ "due date рдирдВрддрд░рдЪреНрдпрд╛ рдордзреНрдпрд░рд╛рддреНрд░реА рдХрд┐рдВрд╡рд╛ рддреНрдпрд╛рдирдВрддрд░". order_runs(fresh) module-level ROLL list рд╡рд╛рдкрд░рдгрд╛рд▒реНрдпрд╛ рддреАрди tests рд╕рд░реНрд╡ 6 рдХреНрд░рдорд╛рдВрдиреА рдЪрд╛рд▓рд╡рддреЗ. flaky_random(rng) 3 рдЧреБрдг рдХрд╛рдврддреЗ; flaky_wait(rng) 200 ms sleep рд╡рд┐рд░реБрджреНрдз рдПрдХ job рд╡реЗрд│ рдХрд╛рдврддреЗ (90% рд╡реЗрд│рд╛ 50 рддреЗ 150 ms, 10% рд╡реЗрд│рд╛ 150 рддреЗ 400 ms). rerun(test, n) test n рд╡реЗрд│рд╛ рдЪрд╛рд▓рд╡рддреЗ рдЖрдгрд┐ passes рдЖрдгрд┐ fails рдореЛрдЬрддреЗ. рдЦрд░реЗ рдШрдбреНрдпрд╛рд│ рдЖрдгрд┐ рдЦрд░реА randomness рдпрд╛рдВрдЪреА рднреВрдорд┐рдХрд╛ demo рдордзреНрдпреЗ seeded generators рдХрд░рддрд╛рдд, рдореНрд╣рдгреВрди рддреНрдпрд╛рдЪреЗ рдЖрдХрдбреЗ рдкреНрд░рддреНрдпреЗрдХ run рдордзреНрдпреЗ рддреЗрдЪ рдпреЗрддрд╛рдд.

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

python3 exam/demo.py flaky
python3 - <<'EOF'
from datetime import datetime
from exam import lab
from exam.lab import Rng, rerun
for hour in (9, 22, 23):
    t = datetime(2026, 9, 1, hour, 30)
    print(f"flaky_time at {hour}:30 тЖТ", "pass" if lab.flaky_time(lambda: t) else "FAIL")
print("fixed_time at any hour тЖТ", "pass" if lab.fixed_time(None) else "FAIL")
print("same seed, 3 runs:", [lab.flaky_random(Rng(42)) for _ in range(3)])
print("fixed data, v3 of the test:", [any(lab.G.grade(s) != "F" for s in (20, 34, 50)) for _ in range(3)])
waits = Rng(13); p, f = rerun(lambda i: lab.flaky_wait(waits))
print(f"sleep 200 ms: passed {p}, failed {f}")
print("order: shared", lab.order_runs(False), "of 6 orders fail ┬╖ fresh", lab.order_runs(True))
EOF

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

flaky рд╣реЗ рдЫрд╛рдкрддреЗ:

   time: due today, handed in within the hour    passed  967 ┬╖ failed  33 тЖТ reads the real clock (fails after 23:00)
   randomness: 3 unseeded scores, one passes     passed  964 ┬╖ failed  36 тЖТ unseeded random data
   waiting: sleep 200 ms, then check the job     passed  924 ┬╖ failed  76 тЖТ a fixed sleep instead of waiting for done
   order: shared ROLL list, all 6 orders of 3 tests тЖТ 5 orders fail ┬╖ fresh ROLL per test тЖТ 0 fail
тФАтФА fixed: an injected clock тЖТ passed 1000, failed 0 ┬╖ a seed тЖТ the same 3 scores every run ┬╖ wait for 'done', not for time

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

flaky_time at 9:30 тЖТ pass
flaky_time at 22:30 тЖТ pass
flaky_time at 23:30 тЖТ FAIL
fixed_time at any hour тЖТ pass
same seed, 3 runs: [True, True, True]
fixed data, v3 of the test: [True, True, True]
sleep 200 ms: passed 924, failed 76
order: shared 5 of 6 orders fail ┬╖ fresh 0

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

рдЪрд╛рд░рдкреИрдХреА рдкреНрд░рддреНрдпреЗрдХ test рдкрд╣рд┐рд▓реНрдпрд╛рдЪ рдкреНрд░рдпрддреНрдирд╛рдд pass рдЭрд╛рд▓реА рдЕрд╕рддреА тАФ failure rates рдкреНрд░рддреНрдпреЗрдХ run рдорд╛рдЧреЗ 3тАУ8% рд╣реЛрддреЗ, рдХрд┐рдВрд╡рд╛ 6 рдкреИрдХреА рдЕрд╕реЗ 5 рдХреНрд░рдо рдЬреЗ runner рдЖрдЬ рдпреЛрдЧрд╛рдпреЛрдЧрд╛рдиреЗ рд╡рд╛рдкрд░рдд рдирд╛рд╣реА. рдлрдХреНрдд рдХрд╛рд╣реАрд╣реА рди рдмрджрд▓рддрд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рд▓реНрдпрд╛рдиреЗ рддреЗ рдЙрдШрдб рдЭрд╛рд▓реЗ. Demo рдордзреНрдпреЗ time test рд╕реБрдорд╛рд░реЗ 30 рдкреИрдХреА 1 run рдордзреНрдпреЗ fail рдЭрд╛рд▓реА, "23:00 рдирдВрддрд░" рд╡рд░реВрди рдЕрдкреЗрдХреНрд╖рд┐рдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ 24 рдкреИрдХреА 1 рдЪреНрдпрд╛ рдЬрд╡рд│. рдмрджрд▓рдгрд╛рд░реА рдЧреЛрд╖реНрдЯ рдПрдХрджрд╛ parameter рдЭрд╛рд▓реА тАФ рдШрдбреНрдпрд╛рд│, рдпрд╛рджреА, seed тАФ рдХреА test рд╣рд╡реА рддреЗрд╡реНрд╣рд╛ 23:30 рд▓рд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддрд╛ рдЖрд▓реА, рдЖрдгрд┐ рд╕реБрдзрд╛рд░рддрд╛ рдЖрд▓реА. Seeded random test рдХрдбреЗ рд▓рдХреНрд╖ рджреНрдпрд╛: seed рддрд┐рд▓рд╛ рдкреБрдиреНрд╣рд╛-рдкреБрдиреНрд╣рд╛ рд╕рд╛рд░рдЦреА рдХрд░рддреЛ, рдмрд░реЛрдмрд░ рдирд╛рд╣реА; рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдЙрдкрд╛рдп рд╣реЛрддрд╛ рдореБрджреНрджрд╛рдо рдирд┐рд╡рдбрд▓реЗрд▓рд╛ data (20, 34, 50).

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

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

рдЦрд▒реНрдпрд╛ project рдордзреНрдпреЗ тАФ freezegun (рдХрд┐рдВрд╡рд╛ time-machine) рдиреЗ рд╡реЗрд│ рдЧреЛрдард╡рд╛, pytest-randomly рдиреЗ рдХреНрд░рдо random рдХрд░рд╛, рдЖрдгрд┐ рд╕рдВрд╢рдпрд┐рдд test pytest-repeat рдиреЗ рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рд╛:

from freezegun import freeze_time
from datetime import datetime, timedelta
from exam.grades import is_late

@freeze_time("2026-09-01 23:30:00")
def test_late_night_hand_in():
    now = datetime.now()                                      # frozen
    assert is_late(now + timedelta(hours=1), now.date())      # the rule, stated for 23:30
pip install pytest-randomly pytest-repeat pytest-rerunfailures
pytest                                     # with pytest-randomly installed: shuffled order, "Using --randomly-seed=тАж"
pytest --randomly-seed=1234                # replay the exact order that failed
pytest --count=200 -x tests/test_report.py # pytest-repeat: run one file 200 times, stop at the first failure

Quarantine рдордзрд▓реНрдпрд╛ рдорд╛рд╣реАрдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ flaky test рд╕рд╛рдареА, pytest-rerunfailures рдлрдХреНрдд рддреБрдореНрд╣реА mark рдХреЗрд▓реЗрд▓реЗрдЪ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддреЗ тАФ рддреНрдпрд╛ рдПрдХрд╛ test рд╡рд░ @pytest.mark.flaky(reruns=2) тАФ рдЖрдгрд┐ рддреЗ mark рдХрд╛рдврдгреНрдпрд╛рд╕рд╛рдареА рдПрдХ ticket.

рдХрд╛рд▓рд╛рд╡рдзреАрдЪреА рдирд╡реНрд╣реЗ, рдЕрдЯреАрдЪреА рд╡рд╛рдЯ рдкрд╛рд╣рд╛ тАФ Playwright рдЪреЗ expect(...).to_have_text(...) timeout рдкрд░реНрдпрдВрдд рдкреБрдиреНрд╣рд╛ рдкреНрд░рдпрддреНрди рдХрд░рддреЗ; рд╕рд╛рдзреНрдпрд╛ code рдордзреНрдпреЗ, deadline рдареЗрд╡реВрди poll рдХрд░рд╛:

import time
def wait_until(cond, timeout=5.0, step=0.05):
    end = time.monotonic() + timeout
    while time.monotonic() < end:
        if cond(): return True
        time.sleep(step)
    raise TimeoutError("condition not met")

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: flaky tests bugs рд╕рд╛рд░рдЦреЗ track рдХрд░рд╛ тАФ рдорд╛рд▓рдХрд╛рдВрд╕рд╣ рдпрд╛рджреА. Flaky test рд▓рд╛ quarantine рдХрд░рд╛ (рддреА рдЪрд╛рд▓рддреЗ, рдкрдг merges рдЕрдбрд╡рдд рдирд╛рд╣реА) тАФ рдорд╣рд┐рдиреЗ рдирд╡реНрд╣реЗ, рджрд┐рд╡рд╕. рдкреНрд░рддреНрдпреЗрдХ failure рдЪрд╛ seed, рдХреНрд░рдо рдЖрдгрд┐ рд╡реЗрд│ рдиреЛрдВрджрд╡рд╛, рдореНрд╣рдгрдЬреЗ рддреЗ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддрд╛ рдпреЗрдИрд▓.

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

рд╕рдЧрд│реЗ рддреБрдХрдбреЗ рдЬрд╛рдЧреЗрд╡рд░ рдЖрд╣реЗрдд. рд╢реЗрд╡рдЯрдЪрд╛ рдзрдбрд╛: tests рдХрдзреА рдЪрд╛рд▓рддрд╛рдд тАФ TDD рддреНрдпрд╛ рдЖрдзреА рд▓рд┐рд╣рд┐рддреЗ, рдЖрдгрд┐ CI рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рд╡рд░ рдпреЛрдЧреНрдп рддреНрдпрд╛ рдЪрд╛рд▓рд╡рддреЗ. рдЖрдгрд┐ рдкрд░реАрдХреНрд╖рд╛ рд╣реЙрд▓рдЪреЗ рд╕рдВрдкреВрд░реНрдг рдЪрд┐рддреНрд░.

git checkout lesson-12-ci-tdd

ЁЯХ░я╕П Lesson 11 тАФ Flaky tests: the wobbly clock on the wall

ЁЯУН You are here: Lesson 11 of 12 ┬╖ Previous: lesson-10-coverage-mutation ┬╖ Next: lesson-12-ci-tdd


ЁЯУж What's in this branch

Lessons 01тАУ10, plus flaky tests: tests that pass and fail on the same code. Four common causes тАФ time, order (shared state), randomness and waiting тАФ each shown by rerunning a test 1000 times with nothing changed, then fixed by pinning down the thing that varies. flaky_time, fixed_time, order_runs, flaky_random, flaky_wait and rerun in exam/lab.py; is_late() in exam/grades.py; flaky() in exam/demo.py.

ЁЯзТ Explain like I'm 5

The exam hall has a clock on the wall that wobbles. ЁЯХ░я╕П Most days it is right. Some days it jumps. A pupil who looks at the clock to decide when to stop writing sometimes gets it wrong тАФ not because of her answers, but because of the clock.

A flaky test is like that. The code did not change, but the test result did.

Aishwarya, who invigilates, finds four wobbly clocks:

  1. Time тП░ тАФ a question checks "homework handed in within the hour is not late". It passes all dayтАж and fails after 23:00, when "within the hour" is tomorrow.
  2. Order ЁЯФА тАФ three questions share one class list. Run them in one order, all pass. Run them in any of the other 5 orders тАФ something fails.
  3. Randomness ЁЯО▓ тАФ a question picks 3 random scores and expects at least one pass. Usually true. Sometimes all three are below 35.
  4. Waiting тП│ тАФ a question starts printing a report card, waits 200 ms, then checks it is done. Usually it is. Sometimes the printer is slow.

Aishwarya's method: run each question 1000 times with nothing changed. A question that both passes and fails is flaky. Then she finds what varies тАФ and nails it down: a fixed clock, a fresh list, a fixed seed, "wait until done".

ЁЯЧ║я╕П Diagram

flowchart LR
    r["ЁЯФБ rerun 1000 times<br/>no code change"] --> t["тП░ time: 33 failed<br/>after 23:00"]
    r --> o["ЁЯФА order: 5 of 6 orders fail"]
    r --> d["ЁЯО▓ randomness: 36 failed"]
    r --> w["тП│ sleep 200 ms: 76 failed"]
    t --> f1["inject a fixed clock тЖТ 0"]
    o --> f2["fresh fixture per test тЖТ 0"]
    d --> f3["fixed seed or fixed data"]
    w --> f4["wait for done, not for time"]

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

тЭУ What

ЁЯдФ Why

Because a flaky suite teaches people to ignore red. Once "just rerun it" is the normal answer, a real failure gets rerun too тАФ and ships. Flakiness also wastes time: in lesson 08's model a suite with 200 e2e tests was red for no reason in 87% of runs. And a flaky test is sometimes the only sign of a flaky product тАФ a real race condition, a real time-zone bug тАФ so each one deserves a look.

ЁЯФз How (in this repo)

Each flaky test in exam/lab.py takes the thing that varies as a parameter, so it can be replayed. flaky_time(clock) calls clock() for the due date and again for the hand-in time plus one hour; is_late() in exam/grades.py says late means "at or after midnight after the due date". order_runs(fresh) runs three tests that share the module-level ROLL list in all 6 orders. flaky_random(rng) draws 3 scores; flaky_wait(rng) draws a job time (90% between 50 and 150 ms, 10% between 150 and 400 ms) against a 200 ms sleep. rerun(test, n) runs a test n times and counts passes and fails. The demo uses seeded generators to play the part of the real clock and real randomness, so its counts are the same every run.

ЁЯзк Try it

python3 exam/demo.py flaky
python3 - <<'EOF'
from datetime import datetime
from exam import lab
from exam.lab import Rng, rerun
for hour in (9, 22, 23):
    t = datetime(2026, 9, 1, hour, 30)
    print(f"flaky_time at {hour}:30 тЖТ", "pass" if lab.flaky_time(lambda: t) else "FAIL")
print("fixed_time at any hour тЖТ", "pass" if lab.fixed_time(None) else "FAIL")
print("same seed, 3 runs:", [lab.flaky_random(Rng(42)) for _ in range(3)])
print("fixed data, v3 of the test:", [any(lab.G.grade(s) != "F" for s in (20, 34, 50)) for _ in range(3)])
waits = Rng(13); p, f = rerun(lambda i: lab.flaky_wait(waits))
print(f"sleep 200 ms: passed {p}, failed {f}")
print("order: shared", lab.order_runs(False), "of 6 orders fail ┬╖ fresh", lab.order_runs(True))
EOF

тЬЕ Verify тАФ what you should see

flaky prints:

   time: due today, handed in within the hour    passed  967 ┬╖ failed  33 тЖТ reads the real clock (fails after 23:00)
   randomness: 3 unseeded scores, one passes     passed  964 ┬╖ failed  36 тЖТ unseeded random data
   waiting: sleep 200 ms, then check the job     passed  924 ┬╖ failed  76 тЖТ a fixed sleep instead of waiting for done
   order: shared ROLL list, all 6 orders of 3 tests тЖТ 5 orders fail ┬╖ fresh ROLL per test тЖТ 0 fail
тФАтФА fixed: an injected clock тЖТ passed 1000, failed 0 ┬╖ a seed тЖТ the same 3 scores every run ┬╖ wait for 'done', not for time

Your snippet prints:

flaky_time at 9:30 тЖТ pass
flaky_time at 22:30 тЖТ pass
flaky_time at 23:30 тЖТ FAIL
fixed_time at any hour тЖТ pass
same seed, 3 runs: [True, True, True]
fixed data, v3 of the test: [True, True, True]
sleep 200 ms: passed 924, failed 76
order: shared 5 of 6 orders fail ┬╖ fresh 0

ЁЯПБ What you just proved

Every one of the four tests would pass on a first try тАФ the failure rates were 3тАУ8% per run, or 5 orders out of 6 that a runner happens not to use today. Only rerunning with nothing changed revealed them. The time test failed about 1 run in 30 in the demo, close to the 1 in 24 you expect from "after 23:00". Once the varying thing was a parameter тАФ a clock, a list, a seed тАФ the test could be replayed at 23:30 on demand, and fixed. Note the seeded random test: a seed makes it repeatable, not right; the honest fix was data chosen on purpose (20, 34, 50).

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ freeze time with freezegun (or time-machine), randomise order with pytest-randomly, and repeat a suspect test with pytest-repeat:

from freezegun import freeze_time
from datetime import datetime, timedelta
from exam.grades import is_late

@freeze_time("2026-09-01 23:30:00")
def test_late_night_hand_in():
    now = datetime.now()                                      # frozen
    assert is_late(now + timedelta(hours=1), now.date())      # the rule, stated for 23:30
pip install pytest-randomly pytest-repeat pytest-rerunfailures
pytest                                     # with pytest-randomly installed: shuffled order, "Using --randomly-seed=тАж"
pytest --randomly-seed=1234                # replay the exact order that failed
pytest --count=200 -x tests/test_report.py # pytest-repeat: run one file 200 times, stop at the first failure

For a known-flaky test in quarantine, pytest-rerunfailures retries only what you mark тАФ @pytest.mark.flaky(reruns=2) on that one test тАФ with a ticket to remove the mark.

Wait for a condition, not a duration тАФ Playwright's expect(...).to_have_text(...) retries until a timeout; in plain code, poll with a deadline:

import time
def wait_until(cond, timeout=5.0, step=0.05):
    end = time.monotonic() + timeout
    while time.monotonic() < end:
        if cond(): return True
        time.sleep(step)
    raise TimeoutError("condition not met")

ЁЯПн Why this matters in production: track flaky tests like bugs тАФ a list with owners. Quarantine a flaky test (it still runs, but does not block merges) for days, not months. Record the seed, the order and the time of every failure, so it can be replayed.

тПня╕П Next

All the pieces are in place. Last lesson: when tests run тАФ TDD writes them first, and CI runs the right ones on every change. And the whole picture of the exam hall.

git checkout lesson-12-ci-tdd
тЖР Previouscoverage mutationNext тЖТci tdd

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