Global Cache, или как выполнить BeforeAll в Playwright один раз для всех воркеров

Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ

Введение

Начнем эту статью с небольшого теста:
Сколько раз выполнится хук BeforeAll в следующем коде на Playwright?

import { test, expect } from '@playwright/test';

test.beforeAll(() => {
  console.log('Executing beforeAll...');
});

test('test 1', () => {
  expect(true).toEqual(true);
});

test('test 2', () => {
  expect(true).toEqual(false);
});

test('test 3', () => {
  expect(true).toEqual(false);
});

👇 Проверьте себя 👇

На первый взгляд кажется, что он должен выполниться один раз перед всеми тестами — как и следует из названия. Но на самом деле он будет вызван 2, 3 или даже больше 4 раз, в зависимости от конфигурации Playwright.

  • В случае с 1 рабочим процессом (воркером) хук будет вызван 2 раза, потому что после падения второго теста Playwright создаст новый воркер и повторно запустит хук BeforeAll для третьего теста:
npx playwright test --workers=1
запуск хука BeforeAll на Playwright
  • Если 3 рабочих процесса и включен fullyParallel, хук будет вызван 3 раза, поскольку каждый тест выполняется в отдельном воркере:
npx playwright test --workers=3 --fully-parallel
запуск хука BeforeAll на Playwright при трех воркерах

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

А получится ли заставить его выполниться более четырех раз?

Проблема параллельного запуска «Test Setup»

Подготовка данных перед выполнением тестов — очень распространенная задача. Иногда нужно аутентифицировать пользователя, инициализировать базу данных или подготовить окружение. Ситуация усложняется, когда тесты запускаются параллельно, поскольку часть подготовительного кода должна выполняться только один раз, даже если рабочих процессов несколько.

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

Читайте также: Параллелизация в Playwright

Пример

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

// auth.spec.ts

test('test authenticated page', () => { ... });
// no-auth.spec.ts

test('test non-authenticated page', () => { ... });

Реализовать аутентификацию можно тремя способами:

  1. через project dependency,
  2. через global setup,
  3. через BeforeAll.

1. Project dependency

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

// playwright.config.ts

export default defineConfig({
  projects: [
    // Setup project
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        // Use prepared auth state.
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ]
});

✅ Преимущества подхода:

  • Аутентификация выполняется только один раз.
  • Шаги логина отображаются в отчете.

❌ Недостатки:

  • Требуется дополнительный проект.
  • Авторизация выполняется всегда, даже для тестов, где она не нужна.

Чтобы продемонстрировать этот пункт, добавим код сброса авторизации в no-auth.spec.ts, как советуют в документации:

// no-auth.spec.ts

// Reset storage state for this file to avoid being authenticated
test.use({ storageState: { cookies: [], origins: [] } });

test('test non-authenticated page', () => { ... });

И запустим только этот файл:

npx playwright test no-auth.spec.ts

Видно, что setup-проект все равно запускает процесс авторизации, хотя это лишнее:

запуск теста с Project dependency

Это определенно место для оптимизации — здесь только на аутентификацию уходят лишние ~2 секунды, хотя можно было обойтись без нее.

2. Global setup

Playwright поддерживает глобальные setup/teardown-скрипты как альтернативу зависимым проектам. Однако этот подход не рекомендуется, потому что у него нет многих возможностей тестового раннера Playwright. Единственная причина использовать эти скрипты — это нежелание добавлять дополнительный проект в конфигурацию Playwright.

✅ Преимущества:

  • Не нужен отдельный проект.
  • Аутентификация выполняется только один раз.

❌ Недостатки:

  • Нет функций раннера Playwright (фикстуры, трассировка и т. д.).
  • Шаги аутентификации не отображаются в отчете.
  • Аутентификация запускается всегда, даже если в ней нет необходимости.

3. BeforeAll

Третий вариант — использовать хук BeforeAll для выполнения авторизации только в файле auth.spec.ts. Он запускается условно, только если выполняется хотя бы один тест из этого набора:

// auth.spec.ts

test.beforeAll(() => {
  console.log('Authenticating...');
});

test('test authenticated page', () => { ... });

Это более оптимальный подход по сравнению с project dependency. Если запускать auth.spec.ts, хук срабатывает:

npx laywright test "/auth.spec.ts"
использование BeforeAll в Playwright в auth.spec.ts

Если запускать no-auth.spec.ts, хук не выполняется:

npx playwright test "/no-auth.spec.ts"
использование BeforeAll в Playwright в no-auth.spec.ts

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

работа BeforeAll при падении теста

Та же ситуация возникает при параллельном выполнении и шардировании.

✅ Преимущества:

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

❌ Недостатки:

  • Выполняется один раз на воркер, а значит, будет повторяться при параллельном запуске или после падений.

Решение с «Global Cache»

Подход с использованием BeforeAll хорош тем, что он позволяет сохранить простоту конфигурации Playwright и выглядит естественно: нужный код запускается перед всеми тестами. Но у него есть минус — повторное выполнение для каждого воркера.

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

Идея проста:

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

Схема работы:

Схема работы модуля Global Cache

Под капотом Global Cache поднимает небольшой HTTP-сервер с простым REST API для получения и записи значений. Этот сервер является единым хранилищем для всех рабочих процессов. Когда воркеру нужно значение, он делает GET-запрос и либо сразу получает закэшированный результат, либо сам считает его и отправляет через POST.

После интеграции с Playwright Global Cache устраняет главный недостаток BeforeAll — гарантирует, что код выполнится ровно один раз. API обернут в интерфейс globalCache с удобными методами:

import { globalCache } from '@vitalets/global-cache';

test.beforeAll(async () => {
  const value = await globalCache.get('key', async () => {
    /* ...heavy calculation, runs once */
    return value;
  });
});

Пример авторизации через Global Cache в BeforeAll:

// auth.spec.ts
import { test } from '@playwright/test';
import { globalCache } from '@vitalets/global-cache';

let storageState;

test.beforeAll(async ({ browser }) => {
  storageState = await globalCache.get('storage-state', async () => {
    console.log('Authentication...');
    const page = await browser.newPage();
    // authentication steps...

    return page.context().storageState();
  });
});

// Set storageState fixture
test.use({ storageState: async ({}, use) => use(storageState) });

test('test authenticated page 1', () => { ... });
test('test authenticated page 2', () => { ... });
test('test authenticated page 3', () => { ... });

Если какой-то тест упадет или будет выполняться параллельно, аутентификация все равно будет выполняться только один раз:

использование модуля GlobalCache в Playwright

BeforeAll может и не понадобиться

Самое интересное наблюдение заключается в том, что хук BeforeAll становится лишним! Код в любом случае гарантированно выполнится только один раз, независимо от того, где он находится. Можно перенести шаги аутентификации прямо в фикстуру storageState (которая запускается перед каждым тестом!). Это упростит код:

// auth.spec.ts
import { test } from '@playwright/test';
import { globalCache } from '@vitalets/global-cache';

test.use({ 
  storageState: async ({ browser }, use) => {
    const storageState = await globalCache.get('storage-state', async () => {
      const page = await browser.newPage();
      // authentication steps...
      return page.context().storageState();
    });
    await use(storageState);
  }
});

test('test authenticated page', () => { ... });

Обратите внимание, что для создания страницы при аутентификации используется вызов browser.newPage(). Если попробовать встроенную фикстуру page, возникнет ошибка циклической зависимости, потому что page зависит от storageState.

Несколько файлов с условной авторизацией

Если у вас несколько файлов с тестами, требующими авторизации, можно вынести логику аутентификации в test.extend(), чтобы она применялась по умолчанию. Отдельные сценарии можно исключить с помощью тегов:

// fixtures.ts
import { test as baseTest } from '@playwright/test';
import { globalCache } from '@vitalets/global-cache';

export const test = baseTest.extend({
  storageState: async ({ storageState, browser }, use, testInfo) => {
    // Skip authentication for '@no-auth'-tagged tests
    if (testInfo.tags.includes('@no-auth')) return use(storageState);

    storageState = await globalCache.get('auth-state', async () => {
      // authentication steps...
    });

    await use(storageState);
  },
});

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

// auth.spec.ts
import { test } from './fixtures';

test('test authenticated page', () => { ... });

Для тестов без аутентификации надо просто добавить тег @no-auth:

// no-auth.spec.ts
import { test } from './fixtures';

test('test non-authenticated page', { tag: '@no-auth' }, () => { ... });

Подведем итог

Global Cache сохраняет простоту хука BeforeAll, но устраняет его главный недостаток — повторное выполнение в каждом воркере. С его помощью можно запускать любой код можно ровно один раз для всех воркеров, даже в режиме параллельного запуска или при шардинге. Это касается не только авторизации, но и заполнения базы данных, тяжелых API-запросов или любой другой общей настройки окружения. Все эти примеры вы также найдете в репозитории проекта: @vitalets/global-cache.

Перевод статьи «Global Cache: Make Playwright BeforeAll Run Once for All Workers».

🔥 Какой была ваша первая зарплата в QA и как вы искали первую работу? 

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Читать в телеграм

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *