QA Interview Hub · База знань · Тести

System Design

Архітектура тест-фреймворку

Шари тест-фреймворку

Масштабований фреймворк - це не папка з тестами, а кілька шарів з чіткими межами. Кожен шар знає тільки про шар під ним.

tests/            - сценарії: що перевіряємо (Given/When/Then)
steps/ або flows/ - бізнес-кроки: createUser, makeTransfer
pages/ + api/     - Page Objects і типізовані API-клієнти
fixtures/         - життєвий цикл: логін, дані, клієнти
config/           - середовища, секрети, таймаути
reporting/        - репортери, артефакти, нотифікації

Навіщо шари. Тест має читатися як бізнес-сценарій: "користувач з балансом 100 переказує 30". Деталі локаторів і HTTP-запитів сховані нижче. Коли змінюється UI, ви правите один Page Object, а не 200 тестів.

Окремо про API-клієнти. У фінтех-проєкті більшість передумов треба створювати через API, а не через UI: створити користувача, пройти KYC, поповнити рахунок. Це в рази швидше і стабільніше. UI залишаємо тільки для того, що перевіряє сам тест.

Фікстури в Playwright - це головний механізм композиції. Через test.extend ви описуєте, що тест отримує готовим: авторизовану сторінку, API-клієнт, користувача з балансом. Фікстура сама прибирає за собою після тесту.

type Fixtures = { api: BankApi; funded: User };

export const test = base.extend<Fixtures>({
  api: async ({ request }, use) => {
    await use(new BankApi(request));
  },
  funded: async ({ api }, use) => {
    const user = await api.createUser();
    await api.deposit(user.accountId, 100);
    await use(user);
    await api.deleteUser(user.id);
  },
});
На співбесіді: Я будую фреймворк шарами: тести описують бізнес-сценарій, кроки і Page Objects ховають деталі, фікстури керують даними і життєвим циклом. Передумови я створюю через API, а UI використовую тільки для того, що реально перевіряю.

Задача: 2000 e2e тестів мають пройти за 15 хвилин

Класична задача. Розбираємо за схемою.

Вимоги і розрахунок

Уточнюємо: середній тест 20 секунд, ціль 15 хвилин на весь набір, запуск на кожен merge в main. Рахуємо: 2000 x 20 с = 40 000 с, це приблизно 667 хвилин послідовно. Залишаємо 3 хвилини на старт контейнерів, встановлення браузерів і злиття звітів, тобто на самі тести 12 хвилин. 667 / 12 = приблизно 56 паралельних слотів. Беремо з запасом: 8 машин (шардів) по 8 воркерів = 64 слоти.

Рішення

  • Ізоляція тестів: кожен тест створює власні дані, немає залежностей між тестами. Інакше паралелізм неможливий.
  • Паралелізм: fullyParallel: true, шардинг через --shard=i/8, звіти у форматі blob і злиття через merge-reports.
  • Передумови через API, логін через збережений storageState.
  • Готовий образ з браузерами і залежностями, щоб не витрачати хвилини на встановлення.
  • Перегляд піраміди: частину з 2000 e2e можна перенести на API-рівень. Це часто дає більше, ніж нові машини.

Trade-offs

Більше машин - вища вартість CI і більше навантаження на тестове середовище. 64 паралельні сесії можуть "покласти" staging, тому середовище теж треба масштабувати або дати кожному шарду своє.

Метрики

Час пайплайна p50 і p95, flaky rate, нерівномірність шардів (найдовший шард проти середнього), вартість одного прогону.

На співбесіді: Я спочатку рахую: 2000 тестів по 20 секунд - це близько 11 годин послідовно, отже потрібно приблизно 60 паралельних слотів. Далі забезпечую ізоляцію даних, шардинг з blob-звітами, готовий Docker-образ і паралельно переглядаю піраміду, бо перенести частину тестів на API часто дешевше, ніж купити машини.

Конфігурація, середовища і секрети

Один фреймворк має запускатися локально, на PR-оточенні і на staging без зміни коду. Тому конфіг - окремий шар.

Принципи:

  • Усі змінні середовища читаються в одному місці і валідуються на старті. Якщо BASE_URL не задано, прогін падає одразу, а не на 300-му тесті.
  • Секрети (ключі API, паролі тестових користувачів) тільки з CI secret store або vault, ніколи в репозиторії.
  • Таймаути задаються централізовано в playwright.config.ts, а не розкидані по тестах.
  • Projects у Playwright розділяють прогони: api, chromium, mobile, smoke. Можна запускати окремі набори через --project.
export default defineConfig({
  fullyParallel: true,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 8 : undefined,
  reporter: process.env.CI ? 'blob' : 'html',
  use: {
    baseURL: env.BASE_URL,
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    { name: 'chromium', dependencies: ['setup'] },
  ],
});

Проєкт setup з залежністю - стандартний спосіб один раз залогінитись і зберегти storageState, який потім використовують усі тести.

Окреме питання - кілька команд. Якщо тести пишуть 5 команд, спільне ядро (клієнти, фікстури, конфіг) варто винести у внутрішній пакет з версіонуванням, а тести тримати поруч із сервісами команд. Тоді ядро розвивається централізовано, а команди не блокують одна одну.

На співбесіді: Я тримаю конфіг окремим шаром: одна точка читання env з валідацією, секрети тільки з CI, таймаути централізовано. Для кількох команд виношу ядро фреймворку в окремий версіонований пакет.