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, таймаути централізовано. Для кількох команд виношу ядро фреймворку в окремий версіонований пакет.