Анатомия юнит-теста: скорость, изоляция, детерминизм
Сцена · три такта одного теста
Любой хороший юнит-тест читается как один абзац из трёх предложений: вот что я приготовил, вот что я сделал, вот что я ожидаю увидеть. Подготовь, сделай, проверь. Если эти три такта смешаны в кашу, тест трудно читать и ещё труднее понять, что именно он проверяет.
А ещё юнит-тест обязан быть быстрым, ни от чего не зависеть и давать один и тот же результат всегда. Эти три свойства, скорость, изоляция, детерминизм, и делают тест юнитом. Разберём каждое, и увидим, что самое коварное из них, детерминизм, в Effect почти бесплатно.
Карта урока · что заберёшь
- разложишь юнит-тест на три свойства: скорость, изоляцию, детерминизм;
- научишься писать в форме Arrange-Act-Assert и поймёшь, зачем это;
- увидишь, как
Date.now()иMath.random()ломают детерминизм; - освоишь
TestClockиRandom.withSeed: время и случайность как управляемые сервисы.
Раздел 1 · что делает тест юнит-тестом
Идея словами
Три свойства, по которым юнит отличается от интеграционного:
- Скорость. Юнит работает за миллисекунды, потому что не выходит за пределы процесса. Тысяча юнитов это секунды, и ты гоняешь их на каждое сохранение.
- Изоляция. Юнит не ходит в сеть, в БД, в файлы и к часам. Он проверяет один модуль, а не оркестр.
- Детерминизм. На один и тот же вход юнит всегда даёт один и тот же ответ. Тест, который иногда зелёный, иногда красный, бесполезнее отсутствия теста, и целый урок впереди про то, почему.
Всё, что мешает любому из трёх, это либо запах в тесте, либо запах в коде. Зашитый поход в сеть убивает изоляцию и скорость. Зашитое Date.now() убивает детерминизм. Лечение почти всегда одно: вынести зависимость наружу.
Картинка · чистое ядро тестируется даром
Вспомни тезис серии из урока про контракты: чем толще чистое ядро, тем дешевле его тестировать. Чистая функция applyPayment по построению обладает всеми тремя свойствами: она мгновенна, ни от чего не зависит и детерминирована, потому что её результат это функция только от аргументов.
// чистое ядро: все три свойства бесплатно
const next = applyPayment(makeAccount({ balance: 100 }), makePayment(30));
expect(next.balance).toBe(70);
Проблемы начинаются на границе: время, случайность, сеть. Туда и смотрим дальше.
Что взять с собой
- юнит это скорость, изоляция, детерминизм;
- чистая функция обладает всеми тремя по построению;
- нарушение свойства это сигнал вынести зависимость наружу.
Раздел 2 · Arrange-Act-Assert
Идея словами
Arrange-Act-Assert это не бюрократия, а способ читаемости. Глаз сразу находит: что было, что произошло, что проверяем. И есть железное правило: один Act на тест. Если в тесте два действия, у него два повода упасть, и при падении непонятно, какое из них сломалось.
Шаг 1 · спагетти-тест
it('платёж', () => {
const acc = makeAccount({ balance: 100 });
expect(applyPayment(acc, makePayment(30)).balance).toBe(70);
const acc2 = makeAccount({ balance: 50 });
expect(() => applyPayment(acc2, makePayment(80))).toThrow();
});
Два разных сценария в одном тесте, проверки перемешаны с подготовкой, имя ничего не говорит. Упадёт, и придётся читать тело, чтобы понять, что именно сломалось.
Шаг 2 · три явные секции
it('списывает сумму и сохраняет инвариант баланса', () => {
// arrange
const account = makeAccount({ balance: 100 });
// act
const next = applyPayment(account, makePayment(30));
// assert
expect(next.balance).toBe(70);
expect(next.history).toHaveLength(1);
});
Второй сценарий (нехватка средств) переезжает в отдельный it с говорящим именем. Теперь имя теста читается как строка спецификации, а тело как абзац документации. Это прямое продолжение идеи теста как ТЗ: хорошо названный юнит и есть исполняемая документация.
Что взять с собой
- три секции: подготовь, сделай, проверь;
- один Act на тест, иначе два повода падать;
- имя теста это строка спецификации.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.