Тестовые двойники и тестовые данные — Примеры
Пример 1 — Тест, который пришпиливает последовательность вызовов
Тест ниже подменяет хранилище моком и утверждает, какие его методы выполнились. Скажите, что произойдёт в следующий раз, когда модуль перестроят, затем замените мок подделкой и утверждайте о результате.
test("сохраняет новый заказ", async () => {
const repo = { insert: vi.fn(), update: vi.fn(), findByKey: vi.fn() };
const service = new OrderService(repo);
await service.place(makeOrder({ key: "abc" }));
expect(repo.findByKey).toHaveBeenCalledWith("abc");
expect(repo.insert).toHaveBeenCalledTimes(1);
expect(repo.update).not.toHaveBeenCalled();
});
Решение
class InMemoryOrders {
rows: Order[] = [];
findByKey(key: string) { return this.rows.find((r) => r.key === key) ?? null; }
insert(order: Order) { this.rows.push(order); return order; }
update(order: Order) { Object.assign(this.rows[0], order); return order; }
}
test("сохраняет размещённый заказ под его ключом", async () => {
const repo = new InMemoryOrders();
await new OrderService(repo).place(makeOrder({ key: "abc" }));
expect(repo.findByKey("abc")).not.toBeNull();
});
Версия с моком описывает текущую реализацию: переставьте поиск и вставку — и она провалится, хотя заказ сохранён. Подделка с настоящим, крошечным поведением позволяет тесту задать вопрос, который задал бы вызывающий, — можно ли достать заказ по его ключу, — поэтому три ожидания о вызовах сводятся к одному утверждению об исходе. Ловушка в том, что мок писать быстрее, чем подделку, так что утверждения о вызовах накапливаются, пока сам файл тестов не становится причиной, по которой никто не рефакторит.
Пример 2 — Фикстура, скопированная пять раз
Литерал заказа ниже повторён в пяти тестах, и в каждом изменено одно поле. Скажите, чего стоит изменение схемы в таком виде, затем замените его фабрикой, которая называет только то, что важно каждому тесту.
const order = {
id: 4471, key: "abc", currency: "GBP", total: 2500,
customer: { id: 9, email: "a@b.test", tier: "gold" },
lines: [{ sku: "book", price: 1000, qty: 2 }],
status: "draft",
}; // также скопирован с суммами 900, 0, 5000 и 1200
Решение
const makeOrder = (overrides: Partial<Order> = {}): Order => ({
id: 4471, key: "abc", currency: "GBP", total: 2500,
customer: { id: 9, email: "a@b.test", tier: "gold" },
lines: [{ sku: "book", price: 1000, qty: 2 }],
status: "draft",
...overrides,
});
// теперь каждый тест называет только свой случай
const order = makeOrder({ total: 900 });
Одно определение держит канонический пример, поэтому новое обязательное поле правится в одном месте, а не в пяти, и каждый тест читается как утверждение о том, что важно именно ему. Ловушка — поверхностное копирование: переопределение customer заменяет весь вложенный объект, поэтому тест с одним изменённым вложенным полем повторяет всех его соседей. Вложенные литералы лучше держать вне переопределений: повторять их стоит, только если тест и правда про всех соседей сразу.