Дублер замість справжньої залежності
Реєстрація користувача зберігає його в базі й надсилає вітальний лист. У unit-тесті не потрібні ні база, ні пошта: замість них стає , обʼєкт з тим самим інтерфейсом, який поводиться так, як треба тесту. Назва з кіно: каскадер-дублер (stunt double) стрибає з даху замість актора.
Слова , , , і походять з книжки Джерарда Месароша (Gerard Meszaros) «xUnit Test Patterns» 2007 року, а відомими їх зробила стаття Мартіна Фаулера (Martin Fowler) «Mocks Aren't Stubs». Бібліотеки вживають їх вільніше: Mockery::mock() створює обʼєкт, який стає stub або mock залежно від того, що ви з ним робите.
Головна різниця в тому, що саме перевіряє тест. дивиться на результат: що повернув метод, що лежить у репозиторії. дивиться на виклики: чи викликали send() рівно раз і з якими аргументами. Перша спокійно переживає рефакторинг, друга від нього ламається, зате бачить дії, які не лишають сліду в стані, як-от надісланий лист.
Дублер стоїть там, де клас говорить зі світом
- RegistrationTest використовує Registration (register()).
- Registration використовує UserRepository.
- Registration використовує Mailer.
- EloquentUsers реалізує UserRepository.
- InMemoryUsers реалізує UserRepository.
- SmtpMailer реалізує Mailer.
- SpyMailer реалізує Mailer.
Один сценарій, пʼять дублерів
Registration реєструє адресу: перевіряє, що її ще немає в UserRepository, зберігає користувача й надсилає лист через Mailer. Перемикайте вкладки: залежності ті самі, міняється лише те, що підставляє і що перевіряє тест.
Запустіть і подивіться
-
1
рядки 8–18
Registration залежить від двох інтерфейсів. Саме на їхнє місце стають дублери, тому класи з new всередині так важко тестувати.
-
2
рядки 38–53
Fake: справжня логіка репозиторію, лише в масиві. Повторну адресу він помітить так само, як база.
-
3
рядки 55–65
Spy: нічого не надсилає, а складає виклики в масив, щоб тест прочитав їх після дії.
-
4
рядки 72–80
Підготували, зробили, перевірили. Перевірки дивляться в стан фейка й у записи спая.
-
5
рядки 82–89
Друга реєстрація тієї самої адреси: виняток, і другого листа немає.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
InMemoryJobRepository у наших тестах
Оновлення каталогу вакансій тестують без бази. Use case SyncJobsFromSource отримує InMemoryJobRepository: той реалізує доменний порт JobRepository, тримає вакансії в масиві й сам видає id, як це зробила б база. Тест перевіряє стан: expect($jobs->jobs)->toHaveCount(2).
Як дублери псують тести
Мок на кожну залежність
Тест підміняє все, крім класу, який перевіряє, і описує кожен виклик. Він повторює реалізацію рядок за рядком і падає від будь-якого рефакторингу, навіть коли поведінка не змінилась.
Натомість: підміняйте лише те, що повільне, недетерміноване або має побічні ефекти: базу, мережу, пошту, час. Value objects і доменні класи беріть справжні.
Мок класу, яким ви не володієте
Тест мокає HTTP-клієнт Guzzle чи SDK платіжної системи й сам вирішує, що вони повернуть. Коли бібліотека змінить поведінку, мок цього не помітить, і зелений тест бреше.
Натомість: загорніть чужий код у власний адаптер з вузьким інтерфейсом, як PaymentGateway. Домен тестуйте з фейком цього адаптера, а сам адаптер integration-тестом проти справжньої бібліотеки або з Http::fake().
Перевірка викликів замість результату
expects('save')->once() там, де можна просто подивитися в репозиторій. Такий тест знає, як код працює всередині, а не що він робить, і ламається, щойно save() замінять на saveMany().
Натомість: спершу перевіряйте стан: що повернулось і що збереглось. Перевірку викликів лишайте для дій без сліду в стані: лист, подія, запит до API.
Фейк, що розійшовся зі справжнім адаптером
InMemory-репозиторій віддає вакансії в порядку додавання, а Eloquent-репозиторій сортує за датою. Тести на фейку зелені, а на продакшні список інший.
Натомість: проганяйте один набір контрактних тестів і на фейку, і на справжньому адаптері, а коли змінюється порт, оновлюйте обидва в тому самому коміті.
Який дублер узяти
- Test double
- Загальна назва для будь-якого обʼєкта, що в тесті замінює справжню залежність. Має той самий інтерфейс, тож код, який тестуємо, різниці не помічає.
- Dummy
- Заповнює параметр і ніколи не використовується. Якщо тест до нього дійшов, це помилка.
- Stub
- Повертає заготовлені відповіді: existsByEmail() завжди false. Про виклики нічого не памʼятає.
- Spy
- Stub, який ще й записує, як його викликали. Тест перевіряє ці записи після дії.
- Mock
- Отримує очікування до дії: має бути один виклик send() з такими аргументами. Сам падає, якщо очікування не справдилось.
- Fake
- Робоча, але спрощена реалізація: репозиторій у масиві замість бази, скринька в памʼяті замість SMTP. Для продакшну не годиться.
- Перевірка стану
- State verification: тест дивиться, що повернув код і що лишилось після нього. Як саме код дійшов до результату, тесту байдуже.
- Перевірка поведінки
- Behaviour verification: тест дивиться, які методи залежностей викликали, скільки разів і з якими аргументами.
Питання зі співбесід
Stub лише повертає заготовлені відповіді, а тест перевіряє результат коду, тобто стан. Mock несе очікування щодо викликів, і тест падає, якщо виклику не було або аргументи інші: це перевірка поведінки. Той самий Mockery::mock() стає stub або mock залежно від того, що ви з ним робите.
Mock отримує очікування до дії, spy записує все й перевіряється після неї. Зі spy тест іде звичним порядком: підготували, зробили, перевірили, а mock строгіший і падає вже на неочікуваному виклику.
Коли залежність тримає стан, який сценарій пише й читає: репозиторій, кеш, черга. Тест з фейком перевіряє результат, а не послідовність викликів, і не ламається від рефакторингу. Ціна: fake треба підтримувати разом з портом і перевіряти тими самими контрактними тестами, що й справжній адаптер.
Мок фіксує ваше припущення про чужий API, і коли бібліотека змінить поведінку, тест цього не побачить. Правило походить з книжки «Growing Object-Oriented Software, Guided by Tests» Стіва Фрімена й Ната Прайса. Чужий код загортають у власний адаптер з вузьким інтерфейсом, домен тестують з його фейком, а сам адаптер перевіряють integration-тестом.