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 лишаю тільки для незмінного конфігу.