Playwright. Фреймворк для быстрого E2E-тестирования

Playwright - фреймворк для E2E-тестирования приложений, он хорошо интегрируется с любыми приложениями и сайтами, предлагает большой ассортимент для кастомизации.

Установка

Установка Playwright достаточно проста. Можно сразу же инициализировать конфигурацию и установить необходимые пакеты:

# pnpm pnpm dlx create-playwright # npm npm init playwright@latest

Если же мы не хотим устанавливать Playwright c дефолтной конфигурацией, то нам нужно:

  • Установить пакет Playwright
  • Самим добавить конфигурацию
pnpm i -D @playwright/test touch playwright.config.js

Конфигурация

Вся конфигурация для Playwright пишется в одном файле - playwright.config.js.

Вот как выглядит базовая версия конфигурации:

import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // Директория для тестов testDir: 'tests', // Нужно ли запускать все тесты параллельно fullyParallel: true, // Данная опция положит все тесты на CI, если хоть в одном тесте установлен test.only forbidOnly: !!process.env.CI, // Сколько будет попыток повторить тест, если он упадет retries: process.env.CI ? 2 : 0, // Настройка количества параллельных воркеров для тестов workers: process.env.CI ? 1 : undefined, // Репортер (в каком формате будет представлена информация о прошедших/не прошедших тестах) reporter: 'html', // Используемые в тестах данные use: { // BaseURL в тестах. Если перейдем на page.goto('/'), то окажемся на localhost:3000 baseURL: 'http://127.0.0.1:3000', // Собирать данные при падении, даже если тест повторяется trace: 'on-first-retry', }, // Конфигурируем браузеры для проекта projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], // Запускаем devServer, перед тем как запустить тесты webServer: { command: 'npm run start', url: 'http://127.0.0.1:3000', reuseExistingServer: !process.env.CI, }, });

К слову, process.env.CI - равен true, только тогда, когда мы запускаем наши тесты внутри CI/CD (Gitlab CI/CD, Github Actions, Jenkins, и так далее).

Продвинутая конфигурация

Вверху был представлен базовый файлик для конфигурации Playwright. Он хорош тем, что является достаточно минималистичным, однако, если нам нужно что-то по сложнее, то нам могут потребоваться дополнительные опции. Внизу предоставлены некоторые из них:

export default defineConfig({ // Директория, в которую будут идти все отчеты, артефакты, видео, скриншоты outputDir: 'test-results', // Файл, который выполнится до начала тестов. Это так называемый бутстрап globalSetup: require.resolve('./global-setup'), // То же самое что и вверху, однако данный файл выполнится после окончания тестов globalTeardown: require.resolve('./global-teardown'), // Таймаут для тестов 💆‍♂️ timeout: 30000, });

Настройки вверху позволяют нам изменить входные и выходные данные для тестов, а также обозначить время, в пределах которого тест будет выполняться.

Не факт, что наши тесты будут лежать в директории test, поэтому мы явно можем указать какие тесты мы хотим обрабатывать, а какие нет, с помощью двух следующих опций:

export default defineConfig({ // Паттерн или регулярное выражение, по которому будет определяться какие тесты будут игнорироваться testIgnore: '*test-assets', // Паттерн или регулярное выражение, по которому будет определяться какие тесты будут выполняться testMatch: '*todo-tests/*.spec.ts', });

Также, нам может потребоваться изменить эмуляционные данные:

  • Время;
  • Геолокацию;
  • Разрешения;
  • ViewPort;
  • Тему;

Все это также можно сделать внутри конфигурации. Поле, которое мы уже ранее обсуждали (use) - отвечает за то, какие внутри браузера будут мета-данные:

export default defineConfig({ use: { // Эмулирует @media (prefers-colors-scheme) colorScheme: 'dark', // Геолокация geolocation: { longitude: 12.492507, latitude: 41.889938 }, // Локализация locale: 'en-GB', // Разрешения permissions: ['geolocation'], // Часовой пояс timezoneId: 'Europe/Paris', // Viewport viewport: { width: 1280, height: 720 }, }, });

Мы также можем изменять параметры сети:

  • Давать разрешение на загрузку;
  • Добавлять кастомные заголовки к HTTP-запросам;
  • Использовать HTTP-Auth;
  • Использовать приложение оффлайн;
  • Использовать прокси;
export default defineConfig({ use: { // Контроллировать разрешение на загрузку файлов acceptDownloads: false, // Добавить кастомные HTTP-заголовки к каждому реквесту extraHTTPHeaders: { 'X-My-Header': 'value', }, // HTTP-Auth httpCredentials: { username: 'user', password: 'pass', }, // Игнорировать ошибки HTTPS во время навигации ignoreHTTPSErrors: true, // Оффлайн offline: true, // Прокси proxy: { server: 'http://myproxy.com:3128', bypass: 'localhost', }, }, });

Также полезно оставлять артефакты, если с тестом что-то не так:

export default defineConfig({ use: { // Делать скриншот, если тест упадет screenshot: 'only-on-failure' // Записывать tracelist после первой неудачной попытки trace: 'on-first-retry', // Записывать видео после первой неудачной попытки video: 'on-first-retry' }, });

Установка необходимых браузеров

Для того чтобы начать пользоваться Playwright необходимо еще установить нужные нам браузеры. По умолчанию Playwright поддерживает Google Chrome на Desktop, однако можно добавить в конфигурацию достаточно много устройств и браузеров.

Мы можем тестировать на:

  • Safari;
  • Edge;
  • Google Chrome;
  • Firefox;

Для того чтобы установить нужные нам браузеры мы должны обратиться к конфигурациии прописать все нужные нам устройства, как это сделано здесь:

// Конфигурируем браузеры для проекта projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, }, { name: 'Mobile Safari', use: { ...devices['iPhone 12'] }, }, ],

Далее нам достаточно просто ввести npx playwright install, playwright сам найдет и скачает нужные нам браузеры.

Тестирование

Само тестирование заключается в том, чтобы провернуть в браузере сценарий, который наиболее вероятно выполнит пользователь.

Playwright предлагает нам управлять браузером программно. Вот как выглядит простой тест на Playwright:

import { test, expect } from '@playwright/test'; test('has title', async ({ page }) => { await page.goto('https://playwright.dev/'); // Проверяем, чтобы страница имела в заголовке Playwright await expect(page).toHaveTitle(/Playwright/); }); test('get started link', async ({ page }) => { await page.goto('https://playwright.dev/'); // Кликаем на ссылку await page.getByRole('link', { name: 'Get started' }).click(); // Проверяем что в URL было слово intro await expect(page).toHaveURL(/.*intro/); });

Нахождение элементов

Для того чтобы производить базовые действия в браузере программно - нам понадобится Locator API .

Сам объект Locator может только находить элементы, все действия (такие как ховер, клик и так далее) находятся в объекте элемента.

Найти элемент с помощью Locator достаточно легко, для этого нам нужно обратиться к Pages API, которое предоставляет нам доступ к странице. Мы можем сами создать новую страницу:

const page = await context.newPage();

И с помощью этой страницы уже перейти на нужную нам страницу (вспоминаем await page.goto(‘url’), или же воспользоваться объектом page, который Playwright автоматически передает в каждый тест-кейс:

// В аргументах к коллбэку мы забираем объект page test('has title', async ({ page }) => { await page.goto('https://playwright.dev/'); await expect(page).toHaveTitle(/Playwright/); });

С помощью page мы можем найти нужный нам элемент. Внизу перечислены несколько способов как это можно сделать:

// Находим по роли page.getByRole('textbox'); // Находим по лэйблу page.getByLabel('Birth date'); // Находим по тексту внутри элемента page.getByText('Item') // Находим по селектору page.locator('#area')

Действия с элементами

Как только мы нашли нужный нам элемент - мы можем проделать с ним какие-либо действия.

Следует упомянуть, что нахождение элемента в Playwright является синхронной операцией. А выполнение действия с элементом - асинхронной.

Внизу предоставлен листинг кода, в котором показано что мы можем сделать с элементом:

// Находим элемент const example = page.locator('#example'); // Кликаем по нему await example.click(); // Активируем чекбокс await example.check(); // Дезактивируем чекбокс await example.uncheck(); // Наводим мышь на элемент await example.hover(); // Заполняем форму (быстро) await example.fill(); // Заполняем форму (медленно, эмулируя реальный ввод) await example.type(); // Фокусируем элемент await example.focus(); // Нажимаем клавишу await example.press(); // Отправляем файлы (Drag n' drop) await example.setInputFiles(); // Выбираем опции из выпадающего списка await example.selectOption();

Если вам нужна полная документация по действиям, то ее можно найти вот тут.

Сравнения

Теперь пришло время для сравнений. Следует сразу же рассказать о том что во всех E2E тестах есть два типа сравнений: промежуточное и финальное.

Промежуточное сравнение помогает понять тест-кейсу правильно ли он выполняет действия и должен ли он дожидаться определенной реакции браузера, прежде чем делать следующее действие.

Например, мы отправили форму и должны увидеть тостер (уведомление) вверху экрана. Прежде чем он появится мы точно знаем, что кнопка отправки должна исчезнуть (или стать снова активной, тут в зависимости от программного решения).

Для того чтобы тест-кейс не бежал сразу проверять есть ли у нас тостер мы можем проверить предварительные условия (кнопка не должна быть видна) или дождаться покуда она видна не будет (что будет означать, что реквест ушел на сервер):

// Данная строка будет дожидаться покуда кнопка исчезнет await page.locator('#button').not.isVisible();

Такие промежуточные сравнения работают без expect, и как уже было сказано ранее - не обрывают тест, если условие ложно, они ждут покуда условие будет правиво в течение определенного времени.

Промежуточные сравнения находятся в Locator API, вот некоторые из них:

// Видим ли элемент await page.locator('#button').isVisible(); // Скрыт ли элемент await page.locator('#button').isHidden(); // Активен ли элемент (нет ли disabled среди атрибутов) await page.locator('#button').isEnabled(); // Неактивен ли элемент (есть ли disabled среди атрибутов) await page.locator('#button').isDisabled(); // Можно ли элемент редактировать await page.locator('#button').isEditable(); // Активен ли чекбокс await page.locator('#button').isChecked();

Финальное сравнение используется тогда, когда нам нужно проверить состояние страницы или элемента на наличие какого-то атрибута, видимости элемента.

Финальное сравнение отличается от промежуточного тем, что заключается в expect. Хорошей практикой является делать одно финальное сравнение в одном тест-кейсе:

expect(await page.locator('#button').isVisible()).toBe(true);

Хорошие практики

Для того чтобы писать хорошие E2E-тесты нужно следовать рекомендациям, иначе можно оказаться в ситуации, что тесты

  • Работают слишком медленно;
  • Влияют на друг-друга;
  • Работают неправильно;

Хорошие практики: Атомарность

Первым и пожалуй одним из самых важных советов является атомарность.

Атомарная ( греч. άτομος — неделимое) операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена.

Тест называется атомарным, когда он из одного состояния приложения переводит приложение в другое состояние, что-то проверяет и возвращает все на свое место.

Представим кейс, где мы переходим по ссылке и проверяем на другой странице какой-то текст. После того как мы проверили текст - мы завершаем тест-кейс. Затем начинается следующий тест-кейс. Он сразу же падает, почему? Потому что следующий тест-кейс ожидает того, что мы начнем тестирование с той же страницы, с которой начинали и предыдущий. Мы же в свою очередь забыли в прошлом тест-кейсе «убрать за собой мусор», то есть оставили приложение висеть на странице, на которую мы перешли.

Для того чтобы такого не допустить, в Playwright, как и во всех популярных фреймворках для тестирования существуют хуки:

  • beforeEach - действие перед каждым тестом;
  • afterEach - действие после каждого теста;
  • beforeAll - действие перед всеми тестами;
  • afterAll - действие после всех тестов;

Если переход со страницы на страницу занимает немного времени, то мы могли зафиксить нашу проблему следюущим образом:

test.beforeEach(async ({page}) => { await page.goto('/'); // или await page.reload(); });

Хорошие практики: Селекторы

В Playwright рекомендуют использовать селекторы, которые вам даст codegen. Codegen по умолчанию не будет цепляться к вашим CSS-селекторам, он будет пытаться цепляться за текст на странице, так как текст меняется реже, нежели селекторы (по крайней мере так думают разработчики Playwright).

Сам codegen можно запустить с помощью команды:

npx playwright codegen

На моей практике оба методы (цепляться за текст и цепляться на CSS-селекторы) показывали себя крайне ненадежно. Можно конечно цепляться за id, однако тогда у элементов будут куча ID’шников в продакшене.

Часто я привязываю специальные селекторы к атрибуту data-test, сами селекторы я заношу в отдельный файл selectors.js и с помощью них уже цепляюсь в тестах. Внизу приведен пример на Vue:

<template> <UniqueElement :data-test="UNIQUE_SELECTOR" class="unique-element" /> </template>

В тестах же, зацепиться за данные селекторы достаточно просто, главное чтобы такие селекторы были уникальными:

await page.locator(`[data-test="`${UNIQUE_SELECTOR}`"]`).click();

Для того чтобы эти селекторы не попадали в продакшн, мы можем просто вырезать их на этапе сборки.

Хорошие практики: Минимальные проверки

Суть минимальных проверок состоит в том, чтобы проверять только те элементы (на видимость, наличие текста и так далее), которые отражают состояние приложения. Допустим у нас отправляется форма и мы ждем покуда лоадер исчезнет. Мы проверим только элемент лоадера и не будем проверять никакие другие элементы на факт их исчезновения.

Если вам сложновато с данной практикой используйте npx playwright codegen и просто выполните действие которое вам нужно. Таким образом вы просто напишите минимальный набор команд, который нужен для того чтобы проверить как приложение отреагировало на пользовательский ввод.

Вместо заключения 🌚

Если вам понравилась данная статья - то вы всегда можете перейти в мой блог, там больше схожей информации о веб-разработке.

Если у вас остались вопросы - не стесняйтесь задавать их в комментариях. Хорошего времяпрепровождения! 💁🏻‍♂

33
Начать дискуссию