ЁЯПл The SchoolтА║ЁЯУМ UIтА║ЁЯзк рдзрдбрд╛ 10 тАФ UI testing: рдлрд▓рдХ рдЕрдЬреВрди рдЙрднрд╛ рдЖрд╣реЗ рдХрд╛?
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯзк рдзрдбрд╛ 10 тАФ UI testing: рдлрд▓рдХ рдЕрдЬреВрди рдЙрднрд╛ рдЖрд╣реЗ рдХрд╛?

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 10 ┬╖ рдорд╛рдЧреАрд▓: lesson-09-performance ┬╖ рдкреБрдвреАрд▓: lesson-11-talking-to-backends


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

рдзрдбреЗ 01тАУ09, рдЖрдгрд┐ рддреНрдпрд╛рд╕реЛрдмрдд рдлрд▓рдХ рди рдореЛрдбрддрд╛ рддреЛ рдХрд╕рд╛ рдмрджрд▓рд╛рдпрдЪрд╛: UI рд╕рд╛рдареА test pyramid (unit, component, end-to-end, visual), рдХрд╛рдп assert рдХрд░рд╛рдпрдЪреЗ (class names рдирд╛рд╣реА, рддрд░ roles рдЖрдгрд┐ labels), рдЖрдгрд┐ рдЖрдзреАрдЪ рдпрд╛ repo рд╡рд░ рдкрд╣рд╛рд░рд╛ рджреЗрдгрд╛рд░рд╛ рдХреЛрдгрддреНрдпрд╛рд╣реА dependency рд╢рд┐рд╡рд╛рдпрдЪрд╛ smoke test. рдЦрд▒реНрдпрд╛ files:

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

рдХреЛрдгреАрд╣реА рдлрд▓рдХ рдкреБрдиреНрд╣рд╛ рд░рдВрдЧрд╡рдгреНрдпрд╛рдЖрдзреА, рддреЛ рдЕрдЬреВрди рдЪрд╛рд▓рддреЛ рдХрд╛ рд╣реЗ рдХреЛрдгреАрддрд░реА рддрдкрд╛рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ. рддрдкрд╛рд╕рдгреНрдпрд╛рдВрдЪреЗ рдЪрд╛рд░ рдкреНрд░рдХрд╛рд░ рдЖрд╣реЗрдд, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреЗ рдкреНрд░рдорд╛рдг рд╡реЗрдЧрд╡реЗрдЧрд│реЗ рд╣рд╡реЗ:

рдЖрдгрд┐ рдХрд╛рдп assert рдХрд░рд╛рдпрдЪреЗ рдпрд╛рдЪрд╛ рдПрдХрдЪ рдирд┐рдпрдо: user рдЬрд╕реЗ рд╡рд┐рдЪрд╛рд░рддреЛ рддрд╕реЗ рд╡рд┐рдЪрд╛рд░рд╛. "Enrol рдирд╛рд╡рд╛рдЪреЗ button", "Name рдЕрд╕реЗ label рдЕрд╕рд▓реЗрд▓реЗ field" тАФ .btn-primary рдХрд┐рдВрд╡рд╛ #f3 > div:nth-child(2) рдирд╛рд╣реА. User рдЪреНрдпрд╛ рднрд╛рд╖реЗрдд рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ tests redesigns рдордзреВрди рдЯрд┐рдХрддрд╛рдд рдЖрдгрд┐ accessibility рддрдкрд╛рд╕рдгреА рдореНрд╣рдгреВрдирд╣реА рдХрд╛рдо рдХрд░рддрд╛рдд: test рд▓рд╛ control рддреНрдпрд╛рдЪреНрдпрд╛ рдирд╛рд╡рд╛рдиреЗ рд╕рд╛рдкрдбрдд рдирд╕реЗрд▓, рддрд░ screen reader рд▓рд╛рд╣реА рд╕рд╛рдкрдбрдд рдирд╛рд╣реА (рдзрдбрд╛ 06).

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

flowchart TB
    u["ЁЯФм unit тАФ many, milliseconds<br/>StudentCard(s) puts the name in"]
    c["ЁЯзй component тАФ some<br/>the form renders, the button is named Enrol"]
    e["ЁЯзн end-to-end тАФ few, slow<br/>a real browser enrols a student"]
    v["ЁЯУ╕ visual тАФ a safety net<br/>screenshot diffs catch unintended CSS"]
    u --> c --> e --> v
    rule["assert by role and label тАФ never by class name"]

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг UI рд╣рд╛ рд▓реЛрдХ рд╕рдЧрд│реНрдпрд╛рдд рдЬрд╛рд╕реНрдд рд╡реЗрд│рд╛ рдмрджрд▓рддрд╛рдд рдЖрдгрд┐ рдЬреНрдпрд╛рд╡рд░ рд╕рдЧрд│реНрдпрд╛рдд рдХрдореА рд╡рд┐рд╢реНрд╡рд╛рд╕ рдареЗрд╡рддрд╛рдд рдЕрд╕рд╛ рдерд░ рдЖрд╣реЗ, рдЖрдгрд┐ "рдореА рдЗрдХрдбреЗрддрд┐рдХрдбреЗ click рдХреЗрд▓реЗ рдЖрдгрд┐ рдареАрдХ рджрд┐рд╕рд▓реЗ" рд╣реЗ рдПрдХрд╛ рдорд╛рдгрд╕рд╛рдкрд▓реАрдХрдбреЗ рдЪрд╛рд▓рдд рдирд╛рд╣реА. рджреЛрди рд╕реЗрдХрдВрджрд╛рдВрдд рдЪрд╛рд▓рдгрд╛рд░рд╛ рджрд╣рд╛-рддрдкрд╛рд╕рдгреНрдпрд╛рдВрдЪрд╛ smoke test рд▓рд╛рдЬрд┐рд░рд╡рд╛рдгреНрдпрд╛ рдкреНрд░рдХрд╛рд░рдЪреЗ bugs рдерд╛рдВрдмрд╡рддреЛ: рд╣рд░рд╡рд▓реЗрд▓реЗ label, рдореЛрдбрд▓реЗрд▓рд╛ endpoint, token рдордзрд▓реА typo.

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

ui/smoke_test.py рдлрдХреНрдд urllib рдЖрдгрд┐ re рд╡рд╛рдкрд░рддреЛ: рддреЛ HTML рдЪреЗ semantics, CSS рдЪреЗ tokens рдЖрдгрд┐ focus style assert рдХрд░рддреЛ, рдЖрдгрд┐ API рд▓рд╛ рджреЛрдирджрд╛ hit рдХрд░рддреЛ тАФ рдПрдХрджрд╛ students рдЪреА рдЕрдкреЗрдХреНрд╖рд╛ рдареЗрд╡реВрди, рдПрдХрджрд╛ рдЪреБрдХреАрдЪреНрдпрд╛ form рд╕рд╛рдареА 400 рдЪреА рдЕрдкреЗрдХреНрд╖рд╛ рдареЗрд╡реВрди. ui/tests/board.spec.js рддреЗрдЪ рджреЛрди рдкреНрд░рд╡рд╛рд╕ Playwright рдордзреНрдпреЗ рджрд╛рдЦрд╡рддреЛ, getByRole рдЖрдгрд┐ getByLabel рд╡рд╛рдкрд░реВрди рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ.

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

python3 ui/mock_api.py &
python3 ui/smoke_test.py; echo "exit code: $?"          # ЁЯОЙ the board stands ┬╖ 0
# 1) break something and watch it fail:
#    remove alt="" from the card <img> in index.html тЖТ тЭМ images have alt, exit 1
# 2) add YOUR check to smoke_test.py (3 lines): the enrol form has a submit button
#    check("form has a submit button", 'type="submit"' in html)
# 3) add a check that the dark theme tokens exist for [data-theme="dark"]
# 4) read ui/tests/board.spec.js and notice: getByRole('button', {name: 'Enrol'}), getByLabel('Name') тАФ
#    the same words a screen reader would say

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

рдирд┐рд░реЛрдЧреА рдлрд▓рдХрд╛рд╡рд░ ЁЯОЙ the board stands рдЖрдгрд┐ exit code 0; alt рдХрд╛рдврд▓реНрдпрд╛рд╡рд░ рдирд╛рд╡ рдЕрд╕рд▓реЗрд▓реА тЭМ рдУрд│ рдЖрдгрд┐ exit 1. рддреБрдордЪреНрдпрд╛ рджреЛрди рдирд╡реАрди рддрдкрд╛рд╕рдгреНрдпрд╛ рдпрд╛рджреАрдд рджрд┐рд╕рддрд╛рдд рдЖрдгрд┐ pass рд╣реЛрддрд╛рдд.

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

рджрд╣рд╛ рд╕реЗрдХрдВрджрд╛рдВрдЪреНрдпрд╛ рддрдкрд╛рд╕рдгреНрдпрд╛ рд╕рдЧрд│реНрдпрд╛рдд рдЬрд╛рд╕реНрдд рддреНрд░рд╛рд╕ рджреЗрдгрд╛рд▒реНрдпрд╛ рдЪреБрдХрд╛ рдкрдХрдбрддрд╛рдд, рдЖрдгрд┐ user рдЪреНрдпрд╛ рднрд╛рд╖реЗрдд рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ tests accessibility tests рдореНрд╣рдгреВрдирд╣реА рдХрд╛рдо рдХрд░рддрд╛рдд.

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

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рд╕рдЧрд│реНрдпрд╛рдд рд╡рд╛рд░рдВрд╡рд╛рд░ рд╣реЛрдгрд╛рд░реЗ, рд╕рдЧрд│реНрдпрд╛рдд рдЬрд╛рд╕реНрдд рджрд┐рд╕рдгрд╛рд░реЗ regressions front end рдордзреНрдпреЗрдЪ рд░рд╛рд╣рддрд╛рдд. CI рдордзрд▓рд╛ рдЫреЛрдЯрд╛рд╕рд╛ smoke test рд╣рд╛ рдХреЛрдгреАрд╣реА UI repo рдордзреНрдпреЗ рдЬреЛрдбреВ рд╢рдХрдгрд╛рд░реЗ рд╕рдЧрд│реНрдпрд╛рдд рдореМрд▓реНрдпрд╡рд╛рди testing рдЖрд╣реЗ тАФ рдореНрд╣рдгреВрдирдЪ рд╣рд╛ repo рддреНрдпрд╛рдЪреНрдпрд╛рд╕рд╣ рдпреЗрддреЛ.

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

рдлрд▓рдХ test рдЭрд╛рд▓рд╛ рдЖрд╣реЗ; рдЖрддрд╛ рддреНрдпрд╛рд▓рд╛ рдЬрдЧрд╛рд╢реА рд╕реБрд░рдХреНрд╖рд┐рддрдкрдгреЗ рдмреЛрд▓реВ рджреНрдпрд╛: backends рд╢реА рдмреЛрд▓рдгреЗ тАФ CORS, tokens, loading рдЖрдгрд┐ error states, XSS.

git checkout lesson-11-talking-to-backends

ЁЯзк Lesson 10 тАФ Testing UI: does the board still stand?

ЁЯУН You are here: Lesson 10 of 12 ┬╖ Previous: lesson-09-performance ┬╖ Next: lesson-11-talking-to-backends


ЁЯУж What's in this branch

Lessons 01тАУ09, plus how to change the board without breaking it: the test pyramid for UI (unit, component, end-to-end, visual), what to assert (roles and labels, not class names), and the zero-dependency smoke test that already guards this repo. Real files:

ЁЯзТ Explain like I'm 5

Before anyone repaints the board, someone should check it still works. There are four kinds of check, and you want different amounts of each:

And the one rule for what to assert: ask the way a user asks. "The button named Enrol", "the field labelled Name" тАФ not .btn-primary or #f3 > div:nth-child(2). Tests written in user language survive redesigns and double as an accessibility check: if the test cannot find the control by its name, neither can a screen reader (lesson 06).

ЁЯЧ║я╕П Diagram

flowchart TB
    u["ЁЯФм unit тАФ many, milliseconds<br/>StudentCard(s) puts the name in"]
    c["ЁЯзй component тАФ some<br/>the form renders, the button is named Enrol"]
    e["ЁЯзн end-to-end тАФ few, slow<br/>a real browser enrols a student"]
    v["ЁЯУ╕ visual тАФ a safety net<br/>screenshot diffs catch unintended CSS"]
    u --> c --> e --> v
    rule["assert by role and label тАФ never by class name"]

тЭУ What

ЁЯдФ Why

Because UI is the layer people change most often and trust least, and because "I clicked around and it looked fine" does not scale past one person. A ten-check smoke test that runs in two seconds prevents the embarrassing class of bug: a missing label, a broken endpoint, a typo in a token.

ЁЯФз How (in this repo)

ui/smoke_test.py uses only urllib and re: it asserts the HTML's semantics, the CSS's tokens and focus style, and hits the API twice тАФ once expecting students, once expecting a 400 for a bad form. ui/tests/board.spec.js shows the same two journeys in Playwright, written with getByRole and getByLabel.

ЁЯзк Try it

python3 ui/mock_api.py &
python3 ui/smoke_test.py; echo "exit code: $?"          # ЁЯОЙ the board stands ┬╖ 0
# 1) break something and watch it fail:
#    remove alt="" from the card <img> in index.html тЖТ тЭМ images have alt, exit 1
# 2) add YOUR check to smoke_test.py (3 lines): the enrol form has a submit button
#    check("form has a submit button", 'type="submit"' in html)
# 3) add a check that the dark theme tokens exist for [data-theme="dark"]
# 4) read ui/tests/board.spec.js and notice: getByRole('button', {name: 'Enrol'}), getByLabel('Name') тАФ
#    the same words a screen reader would say

тЬЕ Verify тАФ what you should see

ЁЯОЙ the board stands and exit code 0 on a healthy board; a named тЭМ line and exit 1 after you remove the alt. Your two new checks appear in the list and pass.

ЁЯПБ What you just proved

Ten seconds of checks catch the mistakes that hurt most, and tests written in user language double as accessibility tests.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: the front end is where the most frequent, most visible regressions live. A tiny smoke test in CI is the highest-value testing anyone adds to a UI repo тАФ it is why this one ships with it.

тПня╕П Next

The board is tested; now make it talk to the world safely: talking to backends тАФ CORS, tokens, loading and error states, XSS.

git checkout lesson-11-talking-to-backends
тЖР PreviousperformanceNext тЖТtalking to backends

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