Друга реалізація порту
У гексагональній архітектурі сценарій бачить лише : інтерфейс JobRepository, JobSource, Mailer. У продакшні на його місці стоїть з базою чи HTTP. Для тестів поруч пишуть ще одну реалізацію того самого інтерфейсу: тримає обʼєкти в масиві, фейковий клієнт віддає заготовлені відповіді й записує, що в нього просили. Такий клас кладуть у tests/Support, і ним користуються сотні тестів.
Для репозиторію fake кращий за mock, бо репозиторій тримає стан. Сценарій синхронізації спершу шукає вакансію, потім зберігає, потім шукає дублі. З моком тест мусить заздалегідь описати кожен виклик і його відповідь, тобто повторити реалізацію. З фейком сценарій викликає що завгодно й у будь-якому порядку, а тест перевіряє, що лишилось у репозиторії. Рефакторинг, який не міняє результату, такий тест не ламає.
Друга перевага в тому, що фейк живе в одному місці. Коли в порту зʼявляється новий метод, PHPStan і PHP самі покажуть, що InMemoryJobRepository його не реалізує, і ви додасте його один раз. Сто моків, розкиданих по тестах, нічого не покажуть, доки їх не запустять.
Фейк може розійтися зі справжнім адаптером: Eloquent сортує за датою, а масив віддає в порядку додавання. Від цього рятує , один набір перевірок, який проганяють і на фейку, і на справжньому адаптері. У Pest це dataset з двох фабрик, у PHPUnit абстрактний клас тесту з методом, що створює реалізацію. Фейкову частину запускають з unit-тестами, справжню з базою в tests/Feature.
Фейки стають на місце адаптерів
- SyncJobsFromSourceTest використовує SyncJobsFromSource (handle()).
- SyncJobsFromSource використовує JobRepository.
- SyncJobsFromSource використовує JobSource.
- EloquentJobRepository реалізує JobRepository.
- InMemoryJobRepository реалізує JobRepository.
- DouRssSource реалізує JobSource.
- ArraySource реалізує JobSource.
Mock, fake і контракт
SyncJobs бере вакансії з джерела й зберігає нові в JobRepository, а ту, що вже є, лише оновлює. Перші дві вкладки перевіряють сценарій з моком і з фейком, третя перевіряє сам фейк контрактним тестом поруч з Eloquent-репозиторієм.
Контракт ловить фейк, що розійшовся
-
1
рядки 8–15
Порт обіцяє порядок: найновіші першими. Ця обіцянка входить у контракт, тож фейк мусить її дотримати.
-
2
рядки 28–45
Фейк на масиві. Він сортує сам, бо так поводиться справжній адаптер з ORDER BY published_at DESC.
-
3
рядки 47–62
Фейк, що розійшовся з портом. Тести сценаріїв на ньому були б зелені, а продакшн показав би інший дайджест.
-
4
рядки 69–81
Контракт описує поведінку порту один раз. У проєкті його запускають і на фейку, і на Eloquent-репозиторії з базою.
-
5
рядки 83–86
Той самий контракт для кожної реалізації. NaiveVacancies провалює дві перевірки з трьох.
-
6
рядки 88–94
Тільки тепер тест сценарію на фейку чогось вартий: фейк поводиться як порт.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
Синхронізація вакансій без бази
Сценарій SyncJobsFromSource оновлює каталог вакансій через адаптер даних. Його unit-тест збирає сценарій з фейків з tests/Support: InMemoryJobRepository й InMemoryCompanyRepository замість бази, RecordingSyncRunLog записує звіти про запуски, ArraySource віддає заготовлений список вакансій і вміє імітувати збій стрічки, а FrozenClock тримає час. Eloquent-реалізації тих самих портів працюють у feature-тестах: tests/Support/SeedsJobs.php наповнює базу через той самий сценарій, узятий з контейнера.
Як фейки підводять
Фейк з логікою, якої немає в адаптері
InMemory-репозиторій сам відкидає застарілі вакансії, хоча в продакшні це робить сценарій. Тести зелені, бо фейк виконує чужу роботу, а з Eloquent правило не спрацьовує.
Натомість: фейк повторює лише поведінку порту: зберегти, знайти, відфільтрувати так, як це зробив би SQL. Бізнес-правила лишаються в домені й сценаріях.
Фейк без контракту
Фейк написали рік тому, відтоді Eloquent-адаптер навчився сортувати й пропускати приховані вакансії. Сотні unit-тестів спираються на поведінку, якої в продакшні вже немає.
Натомість: один контрактний тест на порт, який проганяє ті самі перевірки на фейку й на справжньому адаптері. Змінили поведінку порту, оновили контракт, і він сам покаже, яку реалізацію підтягнути.
Фейк чужого класу замість власного порту
Тест підміняє Guzzle-клієнт чи SDK платіжки фейком, що вгадує формат їхніх відповідей. Коли бібліотека оновиться, фейк цього не помітить.
Натомість: спершу власний порт з вузьким інтерфейсом, як ExchangeRates чи JobSource, і фейк саме для нього. Сам HTTP-адаптер перевіряйте з Http::fake() на записаних відповідях справжнього API.
Фейк, що протікає між тестами
In-memory репозиторій зроблено статичним або синглтоном у контейнері, і вакансії з одного тесту зʼявляються в іншому. Тести проходять поодинці й падають разом.
Натомість: створюйте новий фейк у кожному тесті через new або в beforeEach(). Якщо фейк реєструєте в контейнері, робіть це в тесті, як FakeProjectWeb::install() у PHP Ukraine.
Fake чи mock
- Порт
- Інтерфейс, через який ядро застосунку говорить зі світом: зберегти вакансію, отримати курс, надіслати лист. Описаний мовою домену й живе поруч з ним.
- Адаптер
- Реалізація порту під конкретну технологію: EloquentJobRepository для бази, DouRssSource для RSS-стрічки DOU.
- In-memory репозиторій
- Fake репозиторію, що зберігає обʼєкти в масиві PHP. Сам видає id, шукає й фільтрує, як база, але живе лише до кінця тесту.
- Контрактний тест
- Набір перевірок поведінки порту, який запускають на кожній його реалізації. Якщо фейк і справжній адаптер проходять ті самі перевірки, тестам на фейку можна вірити.
Питання зі співбесід
Реалізація інтерфейсу репозиторію, яка зберігає обʼєкти в масиві PHP замість бази. Вона сама видає id, шукає й фільтрує, тож сценарій працює з нею як з базою, а тест іде мілісекунди й не потребує міграцій.
Репозиторій тримає стан, і сценарій звертається до нього кілька разів: знайти, зберегти, знайти знову. Mock змушує описати кожен виклик і відповідь, тобто повторити реалізацію, і ламається від рефакторингу. Fake памʼятає збережене, тож тест перевіряє результат, а один клас служить усім тестам.
Контрактним тестом: одним набором перевірок, який запускають на кожній реалізації порту. У Pest це dataset з фабриками фейка й справжнього адаптера, у PHPUnit абстрактний тест-клас з методом створення. Справжній адаптер перевіряють на базі в tests/Feature, і якщо поведінка розійдеться, впаде саме та реалізація, що відстала.
Фейк порту підміняє залежність на рівні домену: сценарій навіть не знає, що є HTTP. Http::fake() підміняє відповіді на рівні HTTP-клієнта Laravel і перевіряє сам адаптер: URL, заголовки, розбір JSON. Тести сценаріїв пишуть з фейком порту, а адаптер покривають кількома тестами з Http::fake() на записаних відповідях справжнього API.