Фикстуры и тестовые данные: factory вместо god-объекта
Сцена · общий реквизит на съёмках
Представь съёмочную площадку, где все сцены делят один реквизит. Покрасил стул для одной сцены, а переснимать пришлось десять, потому что во всех кадрах был этот же стул. Так же ведёт себя фикстура, когда она одна на весь набор: меняешь одно поле, и краснеет половина тестов, которые на это поле даже не смотрели.
В этом уроке разберёмся, как готовить тестовые данные так, чтобы каждый тест зависел только от того, что ему важно, а остальное пряталось в разумных дефолтах.
Карта урока · что заберёшь
- поймёшь, чем хрупкая god-фикстура опасна для набора;
- научишься писать factory с дефолтами и overrides;
- узнаешь, когда нужен builder, а когда хватает factory;
- заведёшь фабрики данных для домена Kassa.
Раздел 1 · почему общий объект хрупок
Идея словами
God-фикстура это соблазн новичка: один раз собрал большой объект счёта со всеми полями, и переиспользуй везде. Пока форма не меняется, всё хорошо. Но стоит добавить обязательное поле, как краснеют тесты, которым это поле безразлично. Тест должен падать по своей причине, а не из-за чужого изменения.
Картинка · сломай одно поле
- списывает сумму
- пишет операцию в историю
- отвергает платёж сверх остатка
- считает остаток по истории
- валидирует валюту счёта
- списывает сумму
- пишет операцию в историю
- отвергает платёж сверх остатка
- считает остаток по истории
- валидирует валюту счёта
Нажми кнопку: мы добавляем в объект счёта одно обязательное поле. Слева god-фикстура роняет четыре теста, которые на это поле не смотрят. Справа те же тесты на factory остаются зелёными: новое поле спряталось в дефолтах фабрики, и менять пришлось одно место, а не каждый тест.
Что взять с собой
- god-фикстура связывает тесты, которые не должны быть связаны;
- одно изменение формы роняет половину набора;
- тест должен падать по своей причине.
Раздел 2 · factory с дефолтами и overrides
Идея словами
Factory переворачивает зависимость. Вместо “один объект на всех” получается “функция, которая делает объект, и каждый тест просит только то, что ему нужно”. Релевантное поле на виду, остальное в дефолтах.
Шаг 1 · фабрика счёта и платежа
let seq = 0;
export const makeAccount = (overrides: Partial<Account> = {}): Account => {
seq += 1;
return {
id: `acc_${seq}` as Account['id'],
openingBalance: 100 as Money,
balance: 100 as Money,
history: [],
...overrides,
};
};
export const makePayment = (amount: number, overrides: Partial<Payment> = {}): Payment => ({
amount: amount as Money,
memo: 'test payment',
...overrides,
});
Счётчик seq даёт уникальный id без ручной возни. В тесте видно только важное: makeAccount({ balance: 0 }) кричит “здесь важен нулевой остаток”, а всё прочее не отвлекает.
Шаг 2 · тест читается как одно предложение
it('отвергает платёж сверх остатка', () => {
const reasons = validatePayment(makeAccount({ balance: 50 }), makePayment(100));
expect(reasons).toContain('insufficient-funds');
});
Тест говорит ровно про свой случай. Добавь завтра в Account новое поле, и поправить нужно будет одну фабрику, а не двадцать тестов.
Когда код пишет агент
Агент по умолчанию копипастит полный объект в каждый тест: так быстрее сгенерировать. Получается набор, где двадцать почти одинаковых объектов разъезжаются при первом же изменении схемы. Попроси агента “вынеси создание данных в factory с overrides”, и набор станет устойчивым. Это ровно та работа, которую человек должен заказать явно.
Что взять с собой
- factory отдаёт объект с дефолтами и принимает overrides;
- в тесте на виду только релевантное поле;
- изменение схемы правится в одном месте.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.