Тестирование через реальную границу — Примеры
Пример 1 — Подделка, вернувшая строки в другом порядке
Против подмены в памяти модульный тест проходит. Тот же сценарий на настоящем движке падает. Объясните, какое предположение выдумала подделка, и исправьте код.
// подделка: возвращает строки в порядке вставки, поэтому здесь всё проходит
test("lists orders newest first", async () => {
const fake = { find: async () => [older, newer] };
expect(await listOrders(fake)).toEqual([newer, older]);
});
// настоящий движок: FAIL expected [newer, older], received [older, newer]
// в запросе нет ORDER BY; база вправе вернуть любой порядок
Решение
// проверяем на стыке: то, что вернуло настоящее хранилище
test("lists orders newest first", async () => {
const db = await freshDatabase(); // настоящий движок, своя схема
await db.insert("orders", [older, newer]);
expect(await listOrders(db)).toEqual([newer, older]);
});
// теперь запрос говорит то, что предполагал тест
const rows = await db.query("select * from orders order by created_at desc");
Порядок вставки список в памяти сохранял потому, что так ведёт себя массив JavaScript, а реляционное хранилище без ORDER BY не обещает ничего. Это ровно тот класс предположений, ради которого и существует настоящая граница; изнутри подделки, разделяющей ваше заблуждение, его не поймать. Исправлять нужно запрос, а не тест: контракт, нужный вызывающему коду, сформулировал именно тест, а код ему не соответствовал.
Пример 2 — Тест, зависящий от настенных часов
Сервис регистрации тестируют через его репозиторий. Весь день утверждение проходит, а после полуночи падает у того, кто запустил набор. Назовите недетерминированную часть и сделайте тест устойчивым.
test("counts signups in the last 24 hours", async () => {
const repo = await freshDatabase();
const stats = new SignupStats(repo);
await stats.record("a@example.test");
expect(await stats.recentCount()).toBe(1);
});
// FAIL после полуночи: expected 1, received 0
Решение
const now = new Date("2026-03-01T12:00:00Z");
const repo = await freshDatabase();
const stats = new SignupStats(repo, { now: () => now });
await stats.recordAt("a@example.test", now);
expect(await stats.recentCount()).toBe(1);
// в бою ничего не передают и получают настоящие часы
new SignupStats(repo);
Настоящая инфраструктура обнажает каждое чтение часов в коде. Здесь же это чтение спрятано внутри сервиса. Если передать часы снаружи, утверждение привяжется к фиксированному мгновению, а не к часу, в который случайно запустился набор, — та же дисциплина, что и сортировка строк перед сравнением. Тест, который падает лишь в некоторые дни, учит команду перезапускать прогон вместо разбора; набор, которому не доверяют, стоит меньше того, который никто не написал.