[In one Paper]: Playwright
Playwright. Фреймворк для быстрого E2E-тестирования — Разработка на vc.ru
Тестирование с помощью Playwright
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
и просто выполните действие которое вам нужно. Таким образом вы просто напишите минимальный набор команд, который нужен для того чтобы проверить как приложение отреагировало на пользовательский ввод.