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

Патерни

Singleton і глобальний стан

Singleton у Node і модель воркерів Playwright

Singleton гарантує один екземпляр класу і глобальну точку доступу. У TypeScript його часто роблять не класом зі static getInstance(), а просто експортом об'єкта з модуля, бо модуль кешується і виконується один раз на процес.

Ключовий нюанс Playwright: тести запускаються паралельно в кількох воркер-процесах (окремі процеси Node). Кожен воркер завантажує модулі заново. Отже:

  • Singleton це "один екземпляр на воркер", а не на весь запуск.
  • Стан, записаний в одному воркері, не видно в іншому.
  • Код у globalSetup виконується в окремому процесі, тому змінні, які він заповнив, тести не побачать. Передавати дані треба через файл, змінні оточення або storageState.
// db.ts - "singleton"
let pool: Pool | undefined;
export function getDb() {
  pool ??= new Pool({ connectionString: process.env.DB_URL });
  return pool;
}

При workers: 4 буде 4 пули. Це нормально, але хтось має їх закрити, а в звичайного модуля немає хука для teardown. Ще гірше з мутабельним станом: currentUser у глобальному модулі, який змінюють тести. Порядок тестів у воркері не гарантований після retry, а падіння одного тесту і перезапуск воркера скидає стан.

На співбесіді: У Playwright кожен воркер це окремий процес Node, тому module-level singleton живе окремо в кожному воркері, і спільний стан між ними неможливий. Тому я не тримаю мутабельний глобальний стан, а дорогі ресурси оформлюю як worker-фікстури.

Альтернатива: worker-фікстура замість Singleton

Все, для чого кандидати роблять Singleton (підключення до БД, API-клієнт, конфіг), у Playwright краще робити worker-фікстурою. Вона дає той самий "один екземпляр на воркер", але з керованим життєвим циклом.

export const test = base.extend<{}, { db: Pool }>({
  db: [async ({}, use) => {
    const pool = new Pool({ connectionString: process.env.DB_URL });
    await use(pool);
    await pool.end();
  }, { scope: 'worker' }],
});

Що отримуємо:

  • Явна залежність: видно, які тести використовують БД.
  • Гарантоване закриття ресурсу.
  • Легко підмінити в окремому файлі через новий extend.

Унікальні дані на воркер можна будувати від workerInfo.workerIndex або test.info().parallelIndex. parallelIndex лежить у межах від 0 до workers - 1 і зберігається при перезапуску воркера, тому його зручно використовувати для пулу заздалегідь створених тестових акаунтів.

Коли Singleton допустимий: незмінний конфіг, прочитаний з env один раз. Тут немає стану, який можна зіпсувати.

Типова помилка: "я зробив Singleton для браузера, щоб не відкривати його в кожному тесті". Playwright вже перевикористовує browser на рівні воркера, а ізоляцію тестів дає новий context. Свій singleton-браузер ламає ізоляцію і паралелізм.

На співбесіді: Замість Singleton я роблю worker-фікстури: той самий один екземпляр на воркер, але з явною залежністю і гарантованим teardown. Singleton лишаю тільки для незмінного конфігу.