ЁЯПл The SchoolтА║ЁЯУЭ TestingтА║ЁЯЪж рдзрдбрд╛ 12 тАФ CI рдордзреАрд▓ tests рдЖрдгрд┐ TDD: red, green, refactor тАФ рдЖрдгрд┐ рд╕рдВрдкреВрд░реНрдг рдЪрд┐рддреНрд░
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЪж рдзрдбрд╛ 12 тАФ CI рдордзреАрд▓ tests рдЖрдгрд┐ TDD: red, green, refactor тАФ рдЖрдгрд┐ рд╕рдВрдкреВрд░реНрдг рдЪрд┐рддреНрд░

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 12 ┬╖ рдорд╛рдЧреАрд▓: lesson-11-flaky-tests


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

рдзрдбреЗ 01тАУ11, рдЖрдгрд┐ рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп tests рдХрдзреА рдЪрд╛рд▓рддрд╛рдд. Test-driven development (TDD) рдЙрддреНрддрд░рд╛рдЪреНрдпрд╛ рдЖрдзреА рдкрд░реАрдХреНрд╖реЗрдЪрд╛ рдкреНрд░рд╢реНрди рд▓рд┐рд╣рд┐рддреЗ: red, green, refactor. Continuous integration (CI) рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рд╡рд░ suite рдЪрд╛рд▓рд╡рддреЗ, рд╡реЗрдЧрд╡рд╛рди рддрдкрд╛рд╕рдгреНрдпрд╛ рдЖрдзреА, рдЖрдгрд┐ test selection рдмрджрд▓рд╛рдЪрд╛ рдкрд░рд┐рдгрд╛рдо рд╣реЛрдК рд╢рдХрдгрд╛рд▒реНрдпрд╛ tests рдЪ рдлрдХреНрдд рдЪрд╛рд▓рд╡рддреЗ. grace_v0, grace_v1, grace_v2, GRACE_TESTS, SUITES рдЖрдгрд┐ select_tests() exam/lab.py рдордзреНрдпреЗ; ci() exam/demo.py рдордзреНрдпреЗ. рдЖрдгрд┐ рд╕рдВрдкреВрд░реНрдг рдХреЛрд░реНрд╕ рдПрдХрд╛ рдкрд╛рдирд╛рд╡рд░.

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

рд╢рд╛рд│реЗрдЪрд╛ рдПрдХ рдирд╡рд╛ рдирд┐рдпрдо рдпреЗрддреЛ: "рдЬреА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА 35 рдкрд╛рд╕реВрди 2 рдЧреБрдг рдХрд┐рдВрд╡рд╛ рддреНрдпрд╛рд╣реВрди рдХрдореА рдорд╛рдЧреЗ рдЖрд╣реЗ, рдЖрдгрд┐ рдЬрд┐рдЪреНрдпрд╛рдХрдбреЗ medical note рдЖрд╣реЗ, рддрд┐рд▓рд╛ 35 рдкрд░реНрдпрдВрдд рд╡рд░ рдШреНрдпрд╛рд╡реЗ."

рдХрддрд░рд┐рдирд╛ рдПрдХ рд╡реЗрдЧрд│реАрдЪ рдЧреЛрд╖реНрдЯ рдХрд░рддреЗ. рддреА рдЖрдзреА рдкрд░реАрдХреНрд╖реЗрдЪреЗ рдкреНрд░рд╢реНрди ЁЯУЭ рд▓рд┐рд╣рд┐рддреЗ тАФ рдкрд╛рдЪ: note рд╕рд╣ 33 тЖТ 35; note рд╕рд╣ 34.9 тЖТ 35; note рд╢рд┐рд╡рд╛рдп 33 тЖТ 33; note рд╕рд╣ 32.9 тЖТ 32.9; note рд╕рд╣ 35 тЖТ 35. рддреА рддреЗ рдЪрд╛рд▓рд╡рддреЗ. рдкрд╛рдЪрд╣реА fail рд╣реЛрддрд╛рдд тАФ рдЕрдЬреВрди code рдЪ рдирд╛рд╣реА. ЁЯФ┤ рд╣реЗ red. рдпрд╛рд╡рд░реВрди рд╕рд┐рджреНрдз рд╣реЛрддреЗ рдХреА рдкреНрд░рд╢реНрди fail рд╣реЛрдК рд╢рдХрддрд╛рдд.

рджреАрдкрд┐рдХрд╛ pass рд╣реЛрдгрд╛рд░рд╛ рд╕рд░реНрд╡рд╛рдд рд╕рд╛рдзрд╛ code рд▓рд┐рд╣рд┐рддреЗ. рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рд╛: 5 рдкреИрдХреА 5. ЁЯЯв Green.

рдордЧ рджреАрдкрд┐рдХрд╛ рддреЛ рдиреАрдЯрдиреЗрдЯрдХрд╛ рдХрд░рддреЗ тАФ "2 рдЧреБрдг" рд▓рд╛ рдПрдХ рдирд╛рд╡ рджреЗрддреЗ, рд╢рд╛рд│реЗрдЪрд╛ pass-mark constant рд╡рд╛рдкрд░рддреЗ. рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рд╛: рддрд░реАрд╣реА 5 рдкреИрдХреА 5. ЁЯФ╡ Refactor рдЭрд╛рд▓реЗ, рд╕реБрд░рдХреНрд╖рд┐рддрдкрдгреЗ.

рдЖрдгрд┐ рдЬреЗрд╡реНрд╣рд╛ рдЬреЗрд╡реНрд╣рд╛ рдХреЛрдгреА рдХрд╛рд╣реАрд╣реА рдмрджрд▓рддреЗ, рддреЗрд╡реНрд╣рд╛ рдРрд╢реНрд╡рд░реНрдпрд╛рдЪрд╛ CI рд╣реЙрд▓ ЁЯЪж рдкреНрд░рд╢реНрди рдЖрдкреЛрдЖрдк рдЪрд╛рд▓рд╡рддреЛ: рдЖрдзреА рдЭрдЯрдкрдЯ рдкреНрд░рд╢реНрди, рдЖрдгрд┐ рдлрдХреНрдд рддреНрдпрд╛рдЪ рдкреНрд░рд╢реНрдирдкрддреНрд░рд┐рдХрд╛ рдЬреНрдпрд╛рдВрдЪреНрдпрд╛рд╡рд░ рдмрджрд▓рд╛рдЪрд╛ рдкрд░рд┐рдгрд╛рдо рд╣реЛрдК рд╢рдХрддреЛ. Attendance code рдордзрд▓рд╛ рдмрджрд▓ рд╕рдЧрд│реЗ 105 рдирд╡реНрд╣реЗ, рддрд░ 28 рдкреНрд░рд╢реНрди рдЪрд╛рд▓рд╡рддреЛ. рдкрдг рдХреЛрдгрддреАрд╣реА рдЧреЛрд╖реНрдЯ main branch рдордзреНрдпреЗ рдЬрд╛рдгреНрдпрд╛рдЖрдзреА, рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рд╢реНрдирдкрддреНрд░рд┐рдХрд╛ рд╕реЛрдбрд╡рд▓реА рдЬрд╛рддреЗ.

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

flowchart LR
    red["ЁЯФ┤ RED<br/>5 tests first, 0/5 pass"] --> green["ЁЯЯв GREEN<br/>simplest code, 5/5"]
    green --> ref["ЁЯФ╡ REFACTOR<br/>name the rule, 5/5"]
    ref -->|"push"| ci["ЁЯЪж CI: lint тЖТ unit тЖТ integration тЖТ contract тЖТ e2e"]
    ci --> sel["change to attendance<br/>3 of 6 suites, 28 of 105 tests"]
    ci --> main["merge to main<br/>all 105 tests"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг test suite рдЪрд╛рд▓рд▓рд╛ рддрд░рдЪ рдЙрдкрдпреЛрдЧреА рдЕрд╕рддреЛ тАФ рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рд╡рд░, рддреЛ рдмрджрд▓ рдЗрддрд░ рд╕рдЧрд│реНрдпрд╛рдВрдЪреНрдпрд╛ рдХрд╛рдорд╛рдд рдорд┐рд╕рд│рдгреНрдпрд╛рдЖрдзреА. TDD рдЦрд╛рддреНрд░реА рдХрд░рддреЗ рдХреА tests рдЕрд╕реНрддрд┐рддреНрд╡рд╛рдд рдЖрд╣реЗрдд рдЖрдгрд┐ fail рд╣реЛрдК рд╢рдХрддрд╛рдд; CI рдЦрд╛рддреНрд░реА рдХрд░рддреЗ рдХреА рдХреЛрдгрд╛рд▓рд╛ рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рд╛рд╡реЗ рди рд▓рд╛рдЧрддрд╛ рддреНрдпрд╛ рдЪрд╛рд▓рддрд╛рдд; selection рдЖрдгрд┐ рдХреНрд░рдо feedback рдЗрддрдХрд╛ рд╡реЗрдЧрд╡рд╛рди рдареЗрд╡рддрд╛рдд рдХреА рд▓реЛрдХ рддреНрдпрд╛рд▓рд╛ рд╡рд│рд╕рд╛ рдШрд╛рд▓рдгреНрдпрд╛рдРрд╡рдЬреА рддреНрдпрд╛рдЪреА рд╡рд╛рдЯ рдкрд╛рд╣рддрд╛рдд.

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

exam/lab.py рдордзреАрд▓ GRACE_TESTS рдореНрд╣рдгрдЬреЗ рддреЗ рдкрд╛рдЪ рдкреНрд░рд╢реНрди; run_grace(fn) рддреЗ рдПрдХрд╛ рдЙрдореЗрджрд╡рд╛рд░ function рд╡рд░ рдЪрд╛рд▓рд╡рддреЗ рдЖрдгрд┐ NotImplementedError рд▓рд╛ fail рдорд╛рдирддреЗ. grace_v0 error рджреЗрддреЗ (red), grace_v1 рд╣реА рдкрд╣рд┐рд▓реА pass рд╣реЛрдгрд╛рд░реА version, grace_v2 PASS_MARK рдЖрдгрд┐ lift parameter рд╡рд╛рдкрд░реВрди рдирд┐рдпрдорд╛рд▓рд╛ рдирд╛рд╡ рджреЗрддреЗ. SUITES рд╣реЗ рдЦрд▒реНрдпрд╛ project рдЪреНрдпрд╛ dependency map рдЪреЗ model рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ suite рдЪреНрдпрд╛ tests рдЪреА рд╕рдВрдЦреНрдпрд╛ рдЖрдгрд┐ рддреЛ code рдЪреЗ рдХреЛрдгрддреЗ рднрд╛рдЧ рдЪрд╛рд▓рд╡рддреЛ; select_tests(changed) рдХреЛрдгрддреНрдпрд╛рд╣реА рдмрджрд▓рд▓реЗрд▓реНрдпрд╛ рднрд╛рдЧрд╛рд▓рд╛ рд╕реНрдкрд░реНрд╢ рдХрд░рдгрд╛рд░реЗ suites, рддреНрдпрд╛рдВрдЪреНрдпрд╛ tests рдЪреА рд╕рдВрдЦреНрдпрд╛ рдЖрдгрд┐ рдПрдХреВрдг рд╕рдВрдЦреНрдпрд╛ рдкрд░рдд рджреЗрддреЗ.

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

python3 exam/demo.py ci
python3 - <<'EOF'
from exam import lab
for f in (lab.grace_v0, lab.grace_v1, lab.grace_v2):
    print(f.__name__, [("тЬЕ" if ok else "тЭМ") for _, ok in lab.run_grace(f)])
for changed in ({"store"}, {"report_card"}, {"grade", "attendance"}):
    picked, n, total = lab.select_tests(changed)
    print(f"changed {sorted(changed)} тЖТ {n}/{total} tests from {len(picked)} suites")
EOF
python3 -m unittest discover -s exam/suites -t . 2>&1 | tail -1
python3 exam/demo.py
python3 exam/test_exam.py

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

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

   RED      (test first, no code)  тЖТ 0/5 pass
   GREEN    (simplest code)        тЖТ 5/5 pass
   REFACTOR (name the rule)        тЖТ 5/5 pass
тФАтФА test selection: a change to the attendance client
   runs 3 of 6 suites, 28 of 105 tests: unit/test_attendance, contract/test_attendance_pact, e2e/test_report_flow
   a change to grade() runs 69 of 105 tests ┬╖ merging to main still runs all 105

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

grace_v0 ['тЭМ', 'тЭМ', 'тЭМ', 'тЭМ', 'тЭМ']
grace_v1 ['тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ']
grace_v2 ['тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ']
changed ['store'] тЖТ 16/105 tests from 2 suites
changed ['report_card'] тЖТ 29/105 tests from 2 suites
changed ['attendance', 'grade'] тЖТ 93/105 tests from 5 suites

unittest discover OK рдиреЗ рд╕рдВрдкрддреЗ, рдкреВрд░реНрдг demo тЬЕ done тАФ every paper marked рдиреЗ, рдЖрдгрд┐ checks 12/12 passed рдиреЗ.

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

Red рдЖрдзреА рдЖрд▓реЗ рдЖрдгрд┐ рддреЗ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдард░рд▓реЗ: рдХреЛрдгрддрд╛рд╣реА code рдЕрд╕реНрддрд┐рддреНрд╡рд╛рдд рдпреЗрдгреНрдпрд╛рдЖрдзреА рдкрд╛рдЪрд╣реА рдкреНрд░рд╢реНрди fail рдЭрд╛рд▓реЗ, рдореНрд╣рдгрдЬреЗ рддреЗ fail рд╣реЛрдК рд╢рдХрддрд╛рдд. Refactor рдиреЗ code рдмрджрд▓рд▓рд╛ рдкрдг рдирд┐рдХрд╛рд▓ рдирд╛рд╣реА тАФ green suite рдпрд╛рдЪрд╕рд╛рдареА рдЕрд╕рддреЛ. Selection рдиреЗ store рдордзрд▓реНрдпрд╛ рдмрджрд▓рд╛рд╕рд╛рдареА 105 рдкреИрдХреА рдлрдХреНрдд 16 tests рдареЗрд╡рд▓реНрдпрд╛, рдкрдг grade рдЖрдгрд┐ attendance рд▓рд╛ рд╕реНрдкрд░реНрд╢ рдХрд░рдгрд╛рд▒реНрдпрд╛ рдмрджрд▓рд╛рд▓рд╛ 93 рд▓рд╛рдЧрд▓реНрдпрд╛ тАФ рдмрджрд▓ рдЫреЛрдЯреЗ рдЕрд╕рддреАрд▓ рдЖрдгрд┐ dependency map рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛ selection рд╕рд░реНрд╡рд╛рдд рдЬрд╛рд╕реНрдд рд╡рд╛рдЪрд╡рддреЗ. E2e suite рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рд╕рд╛рдареА рдирд┐рд╡рдбрд▓рд╛ рдЧреЗрд▓рд╛, рдХрд╛рд░рдг рддреЛ рд╕рдЧрд│реНрдпрд╛рд▓рд╛рдЪ рд╕реНрдкрд░реНрд╢ рдХрд░рддреЛ.

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

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

рдЦрд▒реНрдпрд╛ project рдордзреНрдпреЗ тАФ рдПрдХ GitHub Actions workflow, .github/workflows/tests.yml, рдЬреЗ рдпрд╛ repo рдЪреНрдпрд╛ checks рдкреНрд░рддреНрдпреЗрдХ push рдЖрдгрд┐ pull request рд╡рд░, рджреЛрди Python versions рд╡рд░ рдЪрд╛рд▓рд╡рддреЗ:

name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: python3 exam/test_exam.py
      - run: python3 -m unittest discover -s exam/suites -t .
      - run: python3 exam/demo.py > out.txt && diff out.txt demo-output.txt

pytest рд╕реЛрдмрдд рд░реЛрдЬрдЪреНрдпрд╛ рд╡рд╛рдкрд░рд╛рддрд▓реЗ рд╡реЗрдЧ рд╡рд╛рдврд╡рдгрд╛рд░реЗ рдЙрдкрд╛рдп:

pytest -x --ff                 # stop at the first failure, run last-failed tests first
pytest -n auto                 # pytest-xdist: one worker per CPU core
pytest --testmon               # pytest-testmon: only tests affected by the changed code

рдордЧ test job рд▓рд╛ main branch рд╡рд░ required status check рдмрдирд╡рд╛, рдореНрд╣рдгрдЬреЗ рд▓рд╛рд▓ suite merge рдЕрдбрд╡реЗрд▓. CI/CD рд╢рд╛рд│рд╛ рдпрд╛рднреЛрд╡рддреА рдкреВрд░реНрдг pipelines рдмрд╛рдВрдзрддреЗ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: push рдкрд╛рд╕реВрди green рдХрд┐рдВрд╡рд╛ red рдЙрддреНрддрд░ рдорд┐рд│реЗрдкрд░реНрдпрдВрддрдЪрд╛ рд╡реЗрд│ рдореЛрдЬрд╛. рджрд╣рд╛ рдорд┐рдирд┐рдЯрд╛рдВрдкреЗрдХреНрд╖рд╛ рдХрдореА рдЕрд╕реЗрд▓ рддрд░ рд▓реЛрдХ рддреНрдпрд╛рдЪреА рд╡рд╛рдЯ рдкрд╛рд╣рддрд╛рдд; рддреАрд╕рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдЕрд╕реЗрд▓ рддрд░ рд╡рд╛рдЯ рдкрд╛рд╣рдгреЗ рдерд╛рдВрдмрд╡рддрд╛рдд. рд╡реЗрдЧрд╡рд╛рди рдерд░ рд╡реЗрдЧрд╡рд╛рдирдЪ рдареЗрд╡рд╛, рдЬрдореЗрд▓ рддреЗ select рдХрд░рд╛, рдЖрдгрд┐ main рдЪреНрдпрд╛ рдЖрдзреА рд╕рдЧрд│реЗ рдЪрд╛рд▓рд╡рд╛.

ЁЯОУ рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рд╢реНрдирдкрддреНрд░рд┐рдХрд╛ рддрдкрд╛рд╕рд▓реА

рдЙрд╢рд┐рд░рд╛ рд╕рд╛рдкрдбрд▓реЗрд▓рд╛ bug рдЬрд╛рд╕реНрдд рдорд╣рд╛рдЧ рдкрдбрддреЛ, рдЖрдгрд┐ рд╕реЛрдкреЗ рдкреНрд░рд╢реНрди рддреЛ рдЪреБрдХрд╡рддрд╛рдд тЖТ arrange, act, assert, рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ рдкреНрд░рд╢реНрди тЖТ рдХрдбрд╛ 34.5 рд╡рд░рдЪрд╛ bug рд╢реЛрдзрддрд╛рдд тЖТ рдирд╡реНрдпрд╛ рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдирд╛ рд╕реНрд╡рддрдВрддреНрд░ рдареЗрд╡рддрд╛рдд тЖТ рдмрджрд▓реА рдкрд░реАрдХреНрд╖рдХ рдЬреЗ рдЪрд╛рд▓рд╡рддрд╛ рдпреЗрдд рдирд╛рд╣реА рддреНрдпрд╛рдЪреА рдЬрд╛рдЧрд╛ рдШреЗрддрд╛рдд тАФ рдкрдг рдЦреЛрдЯреЗ рдмреЛрд▓реВ рд╢рдХрддрд╛рдд тЖТ рдЦрд░рд╛ database 67.5 рдРрд╡рдЬреА 67 рд╢реЛрдзрддреЛ тЖТ contract рдирд╛рд╡-рдмрджрд▓ ship рд╣реЛрдгреНрдпрд╛рдЖрдзреАрдЪ рдерд╛рдВрдмрд╡рддреЛ тЖТ end-to-end рджрд┐рд╡рд╕ рдереЛрдбреЗ, рдЫреЛрдЯреЗ рдкреНрд░рд╢реНрди рдЦреВрдк тЖТ рдирд┐рдпрдо рдЕрдзрд┐рдХ seed рдЕрдзрд┐рдХ shrinking рд╕реНрд╡рддрдГрд╣реВрди 34.5 рд╢реЛрдзрддрд╛рдд тЖТ mutants рджрд╛рдЦрд╡рддрд╛рдд рдХреА 100% coverage рдиреЗ рдХрд╛рдп рдХрдзреАрдЪ рддрдкрд╛рд╕рд▓реЗ рдирд╛рд╣реА тЖТ рдбреБрдЧрдбреБрдЧрдгрд╛рд░реЗ рдШрдбреНрдпрд╛рд│ рдкрдХреНрдХреЗ рдХреЗрд▓реЗ рдЬрд╛рддреЗ тЖТ рдЖрдгрд┐ CI рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рд╡рд░ рдпреЛрдЧреНрдп рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рддреЗ. рддреБрдореНрд╣реА рдлрдХреНрдд testing рд╢рд┐рдХрд▓рд╛ рдирд╛рд╣реАрдд тАФ рдЖрддрд╛ рддреБрдореНрд╣реА рдХреЛрдгрддрд╛рд╣реА test suite рдкрд╛рд╣реВрди рд╡рд┐рдЪрд╛рд░реВ рд╢рдХрддрд╛: рдХреЛрдгрддреЗ рдкреНрд░рд╢реНрди рд░рд╛рд╣рд┐рд▓реЗ рдЖрд╣реЗрдд, рдХреЛрдгрддреА рдЙрддреНрддрд░реЗ рдЕрдВрджрд╛рдЬ рдЖрд╣реЗрдд, рдЖрдгрд┐ рдХреЛрдгрддреА рдШрдбреНрдпрд╛рд│реЗ рдбреБрдЧрдбреБрдЧрддрд╛рдд? ЁЯУЭЁЯзмЁЯОУ

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

рдЗрддрд░ рд╢рд╛рд│рд╛. CI/CD рд╢рд╛рд│рд╛ рдпрд╛ tests рдЦрд▒реНрдпрд╛ pipelines рдордзреНрдпреЗ рдЪрд╛рд▓рд╡рддреЗ; API рд╢рд╛рд│рд╛ contracts design рдХрд░рддреЗ; Database рд╢рд╛рд│рд╛ marks book рдордзреНрдпреЗ рдЕрдзрд┐рдХ рдЦреЛрд▓рд╡рд░ рдЬрд╛рддреЗ; Observability рд╢рд╛рд│рд╛ tests рдЬреЗ рдкрд╛рд╣реВ рд╢рдХрдд рдирд╛рд╣реАрдд рддреЗ рдкрд╛рд╣рддреЗ; рд╢рд╛рд│рд╛ portal рд╡рд░ рдмрд╛рдХреА рд╕рдЧрд│реЗ рдЖрд╣реЗ.

git checkout main
python3 exam/demo.py     # one last run, for fun

ЁЯЪж Lesson 12 тАФ Tests in CI and TDD: red, green, refactor тАФ and the whole picture

ЁЯУН You are here: Lesson 12 of 12 ┬╖ Previous: lesson-11-flaky-tests


ЁЯУж What's in this branch

Lessons 01тАУ11, plus when tests run. Test-driven development (TDD) writes the exam question before the answer: red, green, refactor. Continuous integration (CI) runs the suite on every change, fast checks first, and test selection runs only the tests a change can affect. grace_v0, grace_v1, grace_v2, GRACE_TESTS, SUITES and select_tests() in exam/lab.py; ci() in exam/demo.py. And the whole course on one page.

ЁЯзТ Explain like I'm 5

A new school rule arrives: "A pupil who is 2 marks or less short of 35, with a medical note, is lifted to 35."

Katrina does something odd. She writes the exam questions first ЁЯУЭ тАФ five of them: 33 with a note тЖТ 35; 34.9 with a note тЖТ 35; 33 without a note тЖТ 33; 32.9 with a note тЖТ 32.9; 35 with a note тЖТ 35. She runs them. All five fail тАФ there is no code yet. ЁЯФ┤ That is red. It proves the questions can fail.

Dipika writes the simplest code that passes. Run again: 5 of 5. ЁЯЯв Green.

Then Dipika tidies it тАФ gives the "2 marks" a name, uses the school's pass-mark constant. Run again: still 5 of 5. ЁЯФ╡ Refactor done, safely.

And every time anyone changes anything, Aishwarya's CI hall ЁЯЪж runs the questions automatically: the quick ones first, and only the papers that the change could affect. A change to the attendance code runs 28 questions, not all 105. But before anything joins the main branch, every paper is sat.

ЁЯЧ║я╕П Diagram

flowchart LR
    red["ЁЯФ┤ RED<br/>5 tests first, 0/5 pass"] --> green["ЁЯЯв GREEN<br/>simplest code, 5/5"]
    green --> ref["ЁЯФ╡ REFACTOR<br/>name the rule, 5/5"]
    ref -->|"push"| ci["ЁЯЪж CI: lint тЖТ unit тЖТ integration тЖТ contract тЖТ e2e"]
    ci --> sel["change to attendance<br/>3 of 6 suites, 28 of 105 tests"]
    ci --> main["merge to main<br/>all 105 tests"]

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

тЭУ What

ЁЯдФ Why

Because a test suite only helps if it runs тАФ on every change, before the change joins everyone else's work. TDD makes sure the tests exist and can fail; CI makes sure they run without anyone remembering to; selection and ordering keep the feedback fast enough that people wait for it instead of working around it.

ЁЯФз How (in this repo)

GRACE_TESTS in exam/lab.py are the five questions; run_grace(fn) runs them against a candidate and treats NotImplementedError as a fail. grace_v0 raises (red), grace_v1 is the first passing version, grace_v2 names the rule with PASS_MARK and a lift parameter. SUITES is a model of a real project's dependency map: each suite's test count and the parts of the code it exercises; select_tests(changed) returns the suites that touch any changed part, their test count and the total.

ЁЯзк Try it

python3 exam/demo.py ci
python3 - <<'EOF'
from exam import lab
for f in (lab.grace_v0, lab.grace_v1, lab.grace_v2):
    print(f.__name__, [("тЬЕ" if ok else "тЭМ") for _, ok in lab.run_grace(f)])
for changed in ({"store"}, {"report_card"}, {"grade", "attendance"}):
    picked, n, total = lab.select_tests(changed)
    print(f"changed {sorted(changed)} тЖТ {n}/{total} tests from {len(picked)} suites")
EOF
python3 -m unittest discover -s exam/suites -t . 2>&1 | tail -1
python3 exam/demo.py
python3 exam/test_exam.py

тЬЕ Verify тАФ what you should see

ci prints:

   RED      (test first, no code)  тЖТ 0/5 pass
   GREEN    (simplest code)        тЖТ 5/5 pass
   REFACTOR (name the rule)        тЖТ 5/5 pass
тФАтФА test selection: a change to the attendance client
   runs 3 of 6 suites, 28 of 105 tests: unit/test_attendance, contract/test_attendance_pact, e2e/test_report_flow
   a change to grade() runs 69 of 105 tests ┬╖ merging to main still runs all 105

Your snippet prints:

grace_v0 ['тЭМ', 'тЭМ', 'тЭМ', 'тЭМ', 'тЭМ']
grace_v1 ['тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ']
grace_v2 ['тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ', 'тЬЕ']
changed ['store'] тЖТ 16/105 tests from 2 suites
changed ['report_card'] тЖТ 29/105 tests from 2 suites
changed ['attendance', 'grade'] тЖТ 93/105 tests from 5 suites

unittest discover ends with OK, the full demo with тЬЕ done тАФ every paper marked, and the checks with 12/12 passed.

ЁЯПБ What you just proved

Red came first and mattered: the five questions failed before any code existed, so they can fail. The refactor changed the code and not the result тАФ that is what the green suite is for. Selection cut a change to the store down to 16 of 105 tests, but a change touching grade and attendance needed 93 тАФ selection saves most when changes are small and the dependency map is honest. The e2e suite was picked for every change, because it touches everything.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ a GitHub Actions workflow, .github/workflows/tests.yml, that runs this repo's checks on every push and pull request, on two Python versions:

name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: python3 exam/test_exam.py
      - run: python3 -m unittest discover -s exam/suites -t .
      - run: python3 exam/demo.py > out.txt && diff out.txt demo-output.txt

Everyday speed-ups with pytest:

pytest -x --ff                 # stop at the first failure, run last-failed tests first
pytest -n auto                 # pytest-xdist: one worker per CPU core
pytest --testmon               # pytest-testmon: only tests affected by the changed code

Then make the test job a required status check on the main branch, so a red suite blocks the merge. The CI/CD school builds whole pipelines around this.

ЁЯПн Why this matters in production: measure the time from push to a green or red answer. Under ten minutes, people wait for it; over thirty, they stop waiting. Keep the fast layers fast, select what you can, and run everything before main.

ЁЯОУ Every paper marked

A bug found late costs more, and easy questions miss it тЖТ arrange, act, assert, one question at a time тЖТ the edges find the bug at 34.5 тЖТ fresh answer sheets keep questions independent тЖТ stand-in examiners replace what you cannot run тАФ but can lie тЖТ a real database finds 67 instead of 67.5 тЖТ a contract stops a rename before it ships тЖТ few end-to-end days, many small questions тЖТ a rule plus a seed plus shrinking finds 34.5 by itself тЖТ mutants show what 100% coverage never checked тЖТ the wobbly clock is pinned down тЖТ and CI asks the right questions on every change. You didn't just learn testing тАФ you can look at any test suite and ask: which questions are missing, which answers are guesses, and which clocks wobble? ЁЯУЭЁЯзмЁЯОУ

тПня╕П Next

The other schools. The CI/CD school runs these tests in real pipelines; the API school designs the contracts; the Database school goes deeper on the marks book; the Observability school watches what tests cannot; the School portal has the rest.

git checkout main
python3 exam/demo.py     # one last run, for fun
тЖР Previousflaky testsFinished! Take the quiz тЖТcheck what stuck

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