Продолжим освоение Playwright. В данной статье настроим автоматический запуск e2e тестов в Gitlab. Сначала в 1 поток, а затем попробуем «на зуб» встроенную многопоточность.
Содержание
- Код проекта
- Файл для Gitlab CI
- Изменения в playwright.config.ts
- Запуск тестов в несколько потоков
- Будет ли быстрее чем в один поток
- Быстрее ли Playwright чем Cypress
- Промежуточные выводы
Код проекта
Для начала нам понадобится какой-либо репозиторий с готовыми тестами. Можем взять за основу Vue3 Realworld Example App, который упоминался в статьях о Cypress. Тем более, что проект перевел свои тесты c Cypress на Playwright.
Для нужд этой статьи пришлось скопировать этот репозиторий в Gitlab под именем Playwright Gitlab CI CD.
Файл для Gitlab CI
Самое важное тут в файле .gitlab-ci.yml:
stages:
- test
variables:
NODE_VERSION: '20'
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
playwright-tests:
stage: test
# Используем официальный образ Playwright с предустановленными браузерами
# https://mcr.microsoft.com/en-us/artifact/mar/playwright/tags
image: mcr.microsoft.com/playwright:v1.55.1-noble
before_script:
- npm ci --legacy-peer-deps || npm install
- npm run build
- mkdir -p test-results
script:
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/ # junit
reports:
junit: test-results/junit.xml
expire_in: 1 week
only:
- merge_requests
- master
- main
Благодаря готовому образу для Docker здесь не так много работы. В секции before_script производим установку зависимостей и сборку приложения. А в секции script — сам запуск тестов.
Есть еще пара нюансов. В артефактах настраиваем junit (машиночитаемый отчёт о тестах), чтобы Gitlab мог показать нам в своем интерфейсе результаты со статистикой в секции Tests (смотрите Рисунок 1).
Изменения в playwright.config.ts
Также пришлось немного поменять вид файла playwright.config.ts, чтобы его проще было читать. До изменений там было много параметров через условия. Например, isCI ? 'on-first-retry' : 'retain-on-failure'. Вот как было до изменений:
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
const isCI = process.env.CI
const baseURL = process.env.E2E_BASE_URL || 'http://localhost:4173'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './playwright',
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!isCI,
/* Retry on CI only */
retries: isCI ? 1 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: isCI
? [
['html', { open: 'never' }],
['list'],
['junit', { outputFile: 'test-results/junit.xml' }],
]
: [
['html', { open: 'never' }],
['list'],
],
globalSetup: './playwright/global.setup.ts',
globalTeardown: './playwright/global.teardown.ts',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
navigationTimeout: isCI ? 10_000 : 20_000,
actionTimeout: isCI ? 10_000 : 20_000,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
screenshot: 'only-on-failure',
trace: isCI ? 'on-first-retry' : 'retain-on-failure',
video: isCI ? 'on-first-retry' : 'retain-on-failure',
},
/* Configure projects for major browsers */
projects: isCI
? [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
]
: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
/* Run your local dev server before starting the tests */
webServer: {
command: isCI ? 'npm run serve' : 'npm run dev',
url: baseURL,
reuseExistingServer: !isCI,
ignoreHTTPSErrors: true,
},
})
И вот как стало:
import { defineConfig, devices } from '@playwright/test'
import type { PlaywrightTestConfig } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
type Environment = 'local' | 'ci'
function getEnvironment(): Environment {
return process.env.CI ? 'ci' : 'local'
}
const baseURL = process.env.E2E_BASE_URL || 'http://localhost:4173'
/**
* Base config with common settings for all environments
*/
const baseConfig: Partial<PlaywrightTestConfig> = {
testDir: './playwright',
fullyParallel: false,
globalSetup: './playwright/global.setup.ts',
globalTeardown: './playwright/global.teardown.ts',
use: {
baseURL,
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
],
}
/**
* Configuration for local development
*/
const localConfig: Partial<PlaywrightTestConfig> = {
forbidOnly: false,
retries: 0,
workers: undefined,
reporter: [
['html', { open: 'never' }],
['list'],
],
use: {
navigationTimeout: 20_000,
actionTimeout: 20_000,
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
webServer: {
command: 'npm run dev',
url: baseURL,
reuseExistingServer: true,
ignoreHTTPSErrors: true,
},
}
/**
* Configuration for CI environment
*/
const ciConfig: Partial<PlaywrightTestConfig> = {
forbidOnly: true,
retries: 1,
workers: 1,
reporter: [
['html', { open: 'never' }],
['list'],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
navigationTimeout: 10_000,
actionTimeout: 10_000,
trace: 'on-first-retry',
video: 'on-first-retry',
},
webServer: {
command: 'npm run serve',
url: baseURL,
reuseExistingServer: false,
ignoreHTTPSErrors: true,
},
}
/**
* Function to create configuration based on environment
*/
function createConfig(env: Environment): PlaywrightTestConfig {
const envConfig = env === 'ci' ? ciConfig : localConfig
// Deep merge for use and webServer
return {
...baseConfig,
...envConfig,
use: {
...baseConfig.use,
...envConfig.use,
},
webServer: envConfig.webServer
? {
...baseConfig.webServer,
...envConfig.webServer,
}
: baseConfig.webServer,
} as PlaywrightTestConfig
}
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig(createConfig(getEnvironment()))
Здесь настройки для CI и локальной работы задаются в отдельных секциях. Так гораздо проще читать код.
Обратите внимание, что для CI задано число воркеров 1 (workers: 1). Если у вас нет своего Gitlab runner, то нет смысла указывать больше.
Запуск тестов в несколько потоков
Для запуска в несколько потоков с тестами нужно несколько ядер. Ниже показан файл .gitlab-ci.yml для Gitlab runner с 4 ядрами:
stages:
- test
variables:
NODE_VERSION: '20'
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
playwright-tests:
stage: test
# Используем официальный образ Playwright с предустановленными браузерами
# https://mcr.microsoft.com/en-us/artifact/mar/playwright/tags
image: mcr.microsoft.com/playwright:v1.55.1-noble
parallel: 4
before_script:
- npm ci --legacy-peer-deps || npm install
- npm run build
- mkdir -p test-results
script:
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
when: always
paths:
- playwright-report/
- test-results/ # junit
reports:
junit: test-results/junit.xml
expire_in: 1 week
only:
- merge_requests
- master
- main
Здесь к предыдущему (однопоточному) варианту было добавлено parallel: 4 и --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL. Это то, с чем Gitlab умеет работать. GitLab автоматически создаёт 4 параллельных задания (job instances) из одного job (playwright-tests), и для каждого из них задаёт переменные окружения: CI_NODE_TOTAL и CI_NODE_INDEX. Описание переменных ниже — в Таблице 1.
| Переменная | Значение | Назначение |
|---|---|---|
CI_NODE_TOTAL | 4 | Общее количество шардов (параллельных задач) |
CI_NODE_INDEX | 1, 2, 3, 4 | Индекс текущей задачи (нумерация с 1) |
На Рисунке 4 можно видеть как в веб-интерфейсе будет выглядеть наш Pipeline:
Будет ли быстрее чем в один поток
И вот мы подошли к месту когда можем сравнить быстроту выполнения тестов в 1 поток и несколько. На примере из данной статьи и Gitlab runner с 4 ядрами получились следующие данные.
- Время выполнения тестов в один поток: 2 минуты 14 секунд
- Время выполнения тестов в два потока: 2 минуты 31 секунда (указано время самого долгого потока)
Можете видеть снимки экрана (Рисунок 3 и Рисунок 4):
Говорят, что это очень частая ситуация в GitLab CI, когда вариант с параллельным шардированием (parallel: 4) оказывается медленнее, хотя ожидается ускорение. Причины этого почти всегда лежат не в самом Playwright, а в окружении GitLab CI и конфигурации пайплайна.
В каждом job есть повторяющиеся действия. Такие как установка зависимостей и сборка. Кажется, что если выделить отдельными задачами это, а результаты добавить в артефакты, то будет быстрее. Однако разворачивание артефактов в новом job — дело не очень быстрое.
Вывод: параллелизм работает только если тестов достаточно много.
Быстрее ли Playwright чем Cypress
Как упоминалось ранее, тестируемый проект ранее использовал Cypress и недавно перешел на Playwright. Таким образом мы можем через git вернуться немного назад, запустить тесты Cypress и сравнить время вариантом для Playwright.
На самом деле, в процессе экспериментов какого-то заметного рывка в производительности замечено не было.
Промежуточные выводы
Playwright имеет docker-образ, который упрощает создание CI задачи для тестов.
Многопоточность поддерживается, как говорится, «из коробки». Без покупки платного сервиса, как у Cypress. Правда, на Gitlab вам нужен собственный runner с несколькими ядрами для этого. А вот при запуске тестов на локальной машине можно реально сэкономить время. Конечно, если тестов достаточно много.
Слухи о том, что Cypress медленнее, чем Playwright на этом этапе не подтвердились.



