Урок 9. Assert — проверки в автотестах
expect и retrying assertions
Темы урока
Библиотека expect, retrying assertions, матчеры (toBeVisible, toHaveValue, toContainText, toHaveCount, toBeEnabled, toHaveURL), негатив через .not, soft assertions, что проверять после Act
Видео урока
Конспект урока
Конспект — Урок 9: Тест зелёный — а в проде баг
Главное за урок
Третья буква AAA — Assert. Это ожидаемый результат из тест-кейса: что пользователь должен увидеть после Act. Не «галочка в конце файла» и не «код не упал».
Урок 6 находил узел. Урок 7 готовил сцену. Урок 8 кликал и заполнял. Сегодня Playwright должен подтвердить, что продукт сделал то, что обещал.
Без Assert автотест проверяет только, что шаги выполнились. Клик прошёл, fill отработал — тест зелёный. В проде поле пустое.
Практика — живой profile-flow.spec.ts. Отдельного Assert Lab нет.
Где кончается Act и начинается Assert
Ручной кейс: предусловия / шаги / ожидаемый. Assert = ожидаемый.
| Часть теста | Что это | Пример |
|---|---|---|
| Arrange | Сцена до действия | makeUser + регистрация + goto профиля |
| Act | То, что проверяем | fill + click «Сохранить» / «Добавить» |
| Assert | Что должно быть на экране | поле содержит новое имя; чип виден; пустой ввод — чипа нет |
click и fill не гарантируют успех: сервер мог не сохранить, UI мог не обновиться. Один дымовой expect с Урока 8 — только «что-то изменилось». Сегодня проверка привязана к смыслу кейса.
Один expect — один смысл. Двенадцать матчеров на один клик — простыня: чинишь по одному, теряешь время.
await expect и забытый await
expect возвращает промис. Без await тест не ждёт проверку: идёт дальше и часто остаётся зелёным, хотя assert не выполнился.
// плохо — промис «ушёл», проверка могла не случиться
const x = expect(page.getByLabel("Имя")).toHaveValue("Анна");
// правильно
await expect(page.getByLabel("Имя")).toHaveValue("Анна");
Правило то же, что с click на Уроке 4 и 8: await перед каждым expect.
Retrying assertions — почему не sleep
Локаторный expect не сравнивает DOM один раз. Он переспрашивает, пока условие не станет true или не истечёт timeout.
UI после клика догоняет — expect ждёт. waitForTimeout(3000) лечит симптом и делает флаки: завтра страница чуть медленнее — снова красный.
Таймаут задаётся в конфиге (expect.timeout) или точечно:
await expect(page.getByRole("button", { name: "Сохранить" })).toBeEnabled({
timeout: 10_000,
});
Не всё в Playwright — retrying. Обычный expect(2 + 2).toBe(4) сравнивает сразу. Retry — у проверок локатора и страницы (toBeVisible, toHaveURL и т.д.).
Какой матчер для чего
Смотри на экран, не в кишки React. Хорошо: текст, value, видимость, disabled. Плохо: внутренний state, точный CSS-класс, порядок вызовов API.
Видимость и DOM
| Матчер | Когда использовать | Пример |
|---|---|---|
toBeVisible() |
элемент на странице и виден пользователю | кнопка «Сохранить», чип навыка, тост |
toBeHidden() |
скрыт: display: none, нулевой размер или нет в DOM |
модалка закрылась |
not.toBeVisible() |
того, чего быть не должно, нет на экране | чип не появился, ошибка не показалась |
toBeAttached() / toBeDetached() |
узел в DOM / уже убрали — даже если невидим | спиннер ушёл из дерева |
toBeInViewport() |
элемент в видимой области, без скролла к нему | sticky-хедер, активный таб |
toBeVisible ≠ «нашёлся в DOM». Элемент может быть attached и при этом скрыт оверлеем или display: none. Для «пользователь это видит» — toBeVisible. Для «узла больше нет» — toBeDetached или toHaveCount(0).
toBeHidden и not.toBeVisible близки, но не близнецы. toBeHidden проходит и для hidden-в-DOM, и для отсутствующего. Если смысл кейса «этого текста нет» — чаще not.toContainText / not.toBeVisible.
Текст и значения полей
| Матчер | Когда использовать | Пример |
|---|---|---|
toHaveText('Профиль') |
точный текст целиком | заголовок, лейбл, одно слово в чипе |
toContainText('Playwright') |
содержит подстроку или regex | чип, алерт, карточка с лишними пробелами |
toHaveValue('Анна') |
value у input / textarea / native select |
имя, Telegram, «О себе» после save |
toHaveValues(['a', 'b']) |
несколько выбранных option в multi-select | фильтры, теги в <select multiple> |
toBeEmpty() |
нет текста и нет детей | пустой список, пустой контейнер |
Не путать: toHaveValue — то, что лежит в поле. toContainText — то, что написано на экране (чип, заголовок, ошибка). Для <input> после fill почти всегда toHaveValue.
await expect(page.getByLabel("Имя")).toHaveValue("Анна Тест");
await expect(page.getByTestId("can-help-skills")).toContainText("Playwright");
Количество
| Матчер | Когда использовать | Пример |
|---|---|---|
toHaveCount(1) |
ровно N узлов | один чип после добавления |
toHaveCount(0) |
список пуст | навыков нет, слотов нет |
toHaveCount(0) — позитивная формулировка «ноль штук». not.toBeVisible() — «этого элемента нет на экране». Для списка чаще count; для одного узла — not.toBeVisible.
Состояние элемента
| Матчер | Когда использовать | Пример |
|---|---|---|
toBeEnabled() |
можно кликнуть / отправить | кнопка после валидной формы |
toBeDisabled() |
нельзя действовать | «Продолжить» без чекбокса оферты |
toBeEditable() |
в поле можно печатать, не readonly |
имя в профиле |
toBeChecked() |
checkbox / radio / switch включён | «Принимаю условия» |
toBeFocused() |
фокус после Tab или автофокуса | первое поле формы |
toBeEnabled / toBeDisabled — про actionability с Урока 8, только уже как проверка, не как ожидание перед кликом.
await expect(page.getByRole("button", { name: "Продолжить" })).toBeDisabled();
await page.getByLabel("Принимаю").check();
await expect(page.getByRole("button", { name: "Продолжить" })).toBeEnabled();
Атрибуты, роли, страница
| Матчер | Когда использовать | Пример |
|---|---|---|
toHaveAttribute('href', '/pomidorqa/profile') |
ссылка, aria-*, data-* |
пункт меню ведёт туда |
toHaveId('email') |
стабильный id | якорь формы |
toHaveRole('alert') |
роль из accessibility tree | сообщение об ошибке |
toHaveAccessibleName('Сохранить') |
имя для скринридера | кнопка без видимого текста |
toHaveClass(/active/) / toContainClass('active') |
CSS-класс | активный таб. Легко стать implementation detail |
toHaveCSS('color', 'rgb(225, 29, 72)') |
computed style | редко нужно в UI-тесте |
toHaveURL(/\/pomidorqa\/?$/) |
навигация после submit | регистрация, логин |
toHaveTitle('PomidorQA') |
<title> вкладки |
редирект на другую страницу |
await expect(page).toHaveURL(/\/pomidorqa\/?$/);
await expect(page.getByRole("link", { name: "Профиль" })).toHaveAttribute(
"href",
"/pomidorqa/profile",
);
toHaveClass и toHaveCSS падают от рефакторинга без бага. Если пользователь видит тот же экран — не проверяй класс .skill-chip.
Ответ API и «голые» значения
| Матчер | Когда использовать | Пример |
|---|---|---|
toBeOK() |
HTTP-ответ успешный | expect(response).toBeOK() |
toEqual / toBe / toContain |
данные, не UI | объект из фабрики, длина массива |
Обычный expect(user.email).toContain('@') не retrying: сравнил один раз и всё. Для экрана — локаторный expect.
Скриншот
toHaveScreenshot() — пиксельное сравнение. В этом курсе не нужно: ломается от шрифта, ретины и «сдвинулось на 2px». База — состояние UI, не картинка.
Негативные проверки: .not
Негатив — такой же assert: ожидаем, что не случилось. В Playwright это .not перед матчером.
await expect(page.getByText("Навык обязателен")).not.toBeVisible();
await expect(page.getByLabel("Имя")).not.toHaveValue("");
await expect(page.getByTestId("can-help-skills")).not.toContainText("Playwright");
await expect(page.getByRole("button", { name: "Добавить" })).not.toBeEnabled();
Типичные кейсы:
| Сценарий | Матчер |
|---|---|
| Элемента нет / скрыт | not.toBeVisible() |
| Текст не появился | not.toContainText('...') |
| Старое значение ушло | not.toHaveValue('старое') |
| Кнопка так и не включилась | not.toBeEnabled() |
| Список пуст | toHaveCount(0) или not.toBeVisible() на элемент списка |
Пустой ввод: required в HTML блокирует submit — чип не появляется. Тест должен это утвердить, а не «кликнул и пошёл дальше».
.not обязателен в ДЗ этого урока: хотя бы один тест с expect(...).not.*.
expect.soft — собрать ошибки разом
Обычный expect: упал — тест остановился. Остальные проверки даже не бегут.
expect.soft: упал — пошёл дальше, в конце красный со списком. Удобно на форме: Имя, Telegram, «О себе» — чинишь все поля за один прогон.
await expect.soft(page.getByLabel("Имя")).toHaveValue("Анна");
await expect.soft(page.getByLabel("Telegram")).toHaveValue("@anna");
await expect.soft(page.getByLabel("О себе")).toHaveValue("Пишу тесты");
Не замена жёсткому assert. Если страница не та — дальше кликать бессмысленно, там нужен обычный expect. Soft — бонус, не обязательная часть ДЗ.
Антипаттерны
| Антипаттерн | Почему плохо |
|---|---|
| Act без Assert | зелёный тест при сломанном продукте |
Забытый await |
проверка не выполнилась |
waitForTimeout перед expect |
флаки, лечит симптом |
| Двенадцать expect на один клик | чинишь по одному |
| Assert в Arrange / в хелпере | хук «проверил» то, что должен проверить тест |
| Implementation details | CSS-класс, внутренний state — красный без бага |
| Только позитив | баг «пустое прошло» не ловится |
expect живёт в тесте, не в beforeEach и не в функции «кликни Добавить». Границы с Page Object — Урок 10.
Как подойти к домашке
git checkout main→git pull origin main, веткаhw9-<github-username>.- Дописать необходимые автотесты на профиль. Минимум 3 теста.
- Один из них — обязательно негативный через
expect(...).not.*. - В каждом тесте после Act есть осмысленный
expect(value, текст, видимость) — не «кликнул и всё». arrange-practiceиbooking-flowне трогать. Без хардкода email. Календарь и бронь не трогаем.- Все тесты зелёные по одному и пачкой → PR в
main.
Ключевые тезисы для теста
- Assert — ожидаемый результат после Act, не «код не упал».
awaitперед каждымexpect; без await проверка может не выполниться.- Локаторный expect переспрашивает UI — sleep перед проверкой не нужен.
toHaveValue— поле;toContainText/toHaveText— текст на экране.toBeVisible— виден пользователю;toBeAttached— просто есть в DOM.- Негатив пишется через
.not(not.toBeVisible,not.toContainText,not.toHaveValue). toHaveCount(0)— список пуст;toBeEnabled/toBeDisabled— можно ли действовать.toHaveURL— навигация после submit.expect.softсобирает несколько ошибок; не заменяет жёсткий expect на развилке.- Не проверяй CSS-класс и внутренний state — смотри на экран.
Домашнее задание
Индивидуальная проверка ДЗ — на Boosty
Конспект, видео и тест открыты всем. Текст задания и проверка работы — по подписке.
Индивидуальная проверка ДЗ на Boosty