Фикстуры, базы данных и очистка — Примеры

Пример 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);
});

Очистка обязана быть безусловной: состояние оставляет после себя именно упавший тест, а разбор, написанный после утверждения, в том важном прогоне как раз и не выполнится. Построение схемы с нуля каждый раз заодно проверяет настоящие миграции — сломанная миграция упадёт здесь, а не во время выпуска. Строку подключения при этом держат в конфигурации с безопасным локальным значением по умолчанию: жёстко вписанный общий хост отделяет от записи туда, где это имеет значение, всего одно копирование.