Фикстуры, базы данных и очистка — Примеры
Пример 1 — Глобальная фикстура, к которой привязан каждый тест
Файл подготовки вставляет пятьдесят клиентов: всё, что может понадобиться тесту, уже лежит на месте. Стоит новому тесту изменить одного из них, и падает другой тест. Покажите, во что фикстура обходится на самом деле, и замените её.
// setup.ts — выполняется один раз для всего набора
beforeAll(async () => {
for (let i = 0; i < 50; i++) await createCustomer({ id: i, tier: "basic" });
});
// тест тихо меняет строку 3
test("upgrades a customer", async () => {
await upgrade(3);
expect(await tierOf(3)).toBe("gold");
});
// поздний, несвязанный тест читает строку 3 и теперь падает
Решение
const customer = await createCustomer({ tier: "gold" }); // только нужное этому случаю
test("upgrades a customer", async () => {
await upgrade(customer.id);
expect(await tierOf(customer.id)).toBe("gold");
});
// разбор безусловен, поэтому следующий тест стартует с того же места
afterEach(async () => { await rollback(); });
Пятьдесят строк, о которых не упоминает ни одно тело теста, привязывают к себе каждый тест, и отказ всплывает не там, где произошло изменение. Небольшая фабрика с одной строкой, разумными значениями по умолчанию и полями, важными для случая, оставляет данные на виду там, где их используют, а последствия сбоя — внутри самого теста. Правило звучит не «меньше данных», а «только данные этого теста»: их создают в тесте и убирают после него, независимо от того, прошёл он или упал.
Пример 2 — Очистка, выполняющаяся только на выходе
Тест вставляет строки, а затем проверяет результат. Если утверждение падает, строки остаются, и очередной прогон видит данные предыдущего; исправлять придётся разбор и строку подключения рядом с ним.
const db = await connect("postgres://admin@shared-db.internal/app");
test("creates an invoice", async () => {
await db.insert("invoices", invoice);
expect(await db.count("invoices")).toBe(1); // FAIL: к третьему дню их уже 3
});
Решение
const url = process.env.TEST_DATABASE_URL ?? "postgres://localhost/app_test";
const db = await connect(url);
beforeEach(async () => { await db.exec("drop schema public cascade; create schema public;"); });
afterEach(async () => { await db.exec("drop schema public cascade; create schema public;"); });
test("creates an invoice", async () => {
await db.insert("invoices", invoice);
expect(await db.count("invoices")).toBe(1);
});
Очистка обязана быть безусловной: состояние оставляет после себя именно упавший тест, а разбор, написанный после утверждения, в том важном прогоне как раз и не выполнится. Построение схемы с нуля каждый раз заодно проверяет настоящие миграции — сломанная миграция упадёт здесь, а не во время выпуска. Строку подключения при этом держат в конфигурации с безопасным локальным значением по умолчанию: жёстко вписанный общий хост отделяет от записи туда, где это имеет значение, всего одно копирование.