awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

DI как шов для тестов: фейки, моки и граница, которая важна

Суть Весь смысл внедрения зависимостей — в шве, который оно создаёт: место, куда подставить тестовый дубль. Но есть два дубля с противоположными целями, и самая частая ошибка тестирования — мокать всё, пока тест не начнёт проверять реализацию вместо поведения.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 16 min

Команда гордится набором тестов OrderService: 100% покрытия, каждая зависимость замокана, всё зелёное. Затем рефакторинг, ничего не меняющий в поведении — разбиение одного метода репозитория на два — делает сорок тестов красными. Тесты не проверяли, что заказы оформляются. Они проверяли, что repo.save вызван ровно один раз ровно с этими аргументами. Шов, который дал им DI, был реальным; они лишь нацелили его не туда.

Шов — это выигрыш

Всё в этом юните — внедрение через конструктор, корень композиции, абстракции вместо new — окупается здесь. Поскольку OrderService получает PaymentGateway, а не конструирует StripeClient, тест может передать замену. Эта замена — тестовый дубль, а точка внедрения — шов: стык, где продакшен-связывание меняется на тестовое. Нет шва — нет изолированного юнит-теста. Поэтому «тестируемо ли это?» и «внедрены ли зависимости?» — почти один вопрос.

Два дубля, противоположные цели

Слово «мок» используют вольно для любой замены, но различие — весь урок:

  • Стаб / фейк заменяет зависимость и поставляет состояние. Фейковый UserRepository на in-memory Map ведёт себя как реальный: сохранил пользователя — можешь прочитать обратно. Ваши проверки смотрят на результат — заказ оказался сохранён, возвращённая сумма верна.
  • Мок запрограммирован ожиданиями про вызовы. Он утверждает, что payment.charge(amount) вызван один раз с этим аргументом. Ваши проверки смотрят на взаимодействие, не на исход.

Первый проверяет, что система сделала; второй — как она это сделала. Оба легитимны, но падают по-разному — и Hook это то, что бывает, когда мок используют для того, что должен был покрыть фейк.

Классицисты против лондонцев и почему это важно

Это раскол классицисты против мокистов («лондонская школа»). Мокисты мокают каждого коллаборатора и проверяют взаимодействия, так что каждый юнит тестируется в полной изоляции. Классицисты используют реальные объекты или фейки для коллабораторов, которыми владеют, и берегут моки для неудобных границ. Практическое следствие — связанность со структурой: полностью замоканный тест знает точную форму вызова своей зависимости, поэтому любой рефакторинг, сохраняющий поведение, но меняющий форму вызова, ломает тест. Это и есть баг сорока красных тестов. Тесты, проверяющие через состояние, переживают рефакторинги, ведь им важен лишь наблюдаемый результат.

Почему это работает

Почему тесты взаимодействий ломаются на рефакторингах, ничего не меняющих? Потому что ожидание мока есть утверждение о реализации. expect(repo.save).toHaveBeenCalledTimes(1) кодирует «продакшен-код вызывает save ровно один раз». Разбейте это на два save внутри транзакции — идентичное поведение, идентичное финальное состояние — и ожидание теперь ложно, хотя ничто наблюдаемое пользователем не изменилось. Тест мерил внутренние ходы кода, не его вывод. У тестов на состоянии этой проблемы нет: они спрашивают «после прогона заказ сохранён и сумма верна?», что инвариантно к любому рефакторингу, сохраняющему поведение. Моки не неправильны — они верный инструмент для проверки эффекта, который нельзя наблюдать через состояние, вроде «письмо отправлено» — но каждый мок это маленькая ставка, что именно эта форма вызова часть контракта.

Мокай на границе, фейкай то, чем владеешь

Дисциплина, избегающая чрезмерного мокания: мокай на краях системы, используй реальные объекты или фейки внутри неё. Код, которым владеешь и управляешь — доменные сервисы, твои репозитории — можно связать реальными экземплярами или in-memory-фейками, чтобы тесты упражняли настоящее взаимодействие. Мокать стоит границы, которыми не управляешь или которые не можешь позволить в тесте: платёжный шлюз, отправитель писем, часы, сторонний HTTP-вызов. Это ровно те зависимости, где хочешь утверждать «мы вызвали Stripe с этой суммой», ведь сам вызов и есть внешне видимый эффект. Шов ценнее всего именно на границе системы — где DI и важнее всего.

Чрезмерное мокание — запах дизайна

Когда юнит-тесту нужно десять моков, чтобы сконструировать субъект, проблема не в тесте — в дизайне. Класс, требующий десять коллабораторов, делает слишком много, и болезненный тест — гонец. Рефлекс сеньора — читать боль теста как обратную связь о связанности, а не как повод тянуться за бо́льшим мок-арсеналом. Трудно тестировать обычно значит трудно менять.

ДубльПоставляетВы проверяетеЛомается на
Фейк / стабРеалистичное состояниеРезультат/исходТолько смена поведения
МокЗаписанные ожиданияВзаимодействие (вызовы)Любая смена формы вызова
Реальный объектНастоящее поведениеРезультат/исходТолько смена поведения
Викторина

Сохраняющий поведение рефакторинг разбивает один `repo.save()` на два save внутри транзакции, и десятки тестов краснеют. Что это вскрывает о тех тестах?

Викторина

Какую зависимость лучше всего заменить моком, проверяющим вызов, а не фейком, поставляющим состояние?

Викторина

Юнит-тесту нужно десять моков лишь чтобы инстанцировать тестируемый класс. Каково сеньорское прочтение этой боли?

Вспомните перед уходом
  1. 01
    Что такое шов для тестов и как DI его создаёт?
  2. 02
    В чём разница между фейком/стабом и моком и как они падают по-разному?
  3. 03
    Что за правило «мокай на границе, фейкай то, чем владеешь» и почему чрезмерное мокание сигналит проблему дизайна?
Итог

Шов, который создаёт внедрение зависимостей, — вся причина, по которой тестируемость и внедрение — один разговор: точка внедрения — это место, где продакшен-связывание уступает тестовому дублю. Но «дубль» прячет развилку. Фейк или стаб поставляет реалистичное состояние и даёт проверкам смотреть на исход, поэтому ломается лишь при настоящей смене поведения; мок записывает ожидания вызовов и проверяет взаимодействия, поэтому ломается на любом рефакторинге, меняющем форму вызова — причина того, что сохраняющее поведение изменение делает десятки тестов красными. Классицистская дисциплина держит тесты надёжными: мокай границы, которыми не владеешь (платёж, письма, часы, внешний HTTP), где сам вызов — видимый эффект, и связывай реальные объекты или фейки для кода, которым управляешь, проверяя через состояние. А когда тесту нужно десять моков лишь чтобы поставить субъект на ноги, боль — это дизайн говорит: слишком много коллабораторов, слишком много ответственности. С понятым швом последний урок поворачивает к тому, что делает реальный DI-контейнер в продакшене: графы разрешения, циклические зависимости, жадный старт и когда не использовать контейнер вовсе.

Связанные уроки
встречается в185
Продолжить восхождение ↑DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоит
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.