<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
ТЕСТУВАННЯ · MIDDLE ЧАСТО ПИТАЮТЬ

Що варто підміняти в тестах, а що ні?

Підміняють те, що повільне, недетерміноване або має побічні ефекти: базу, мережу, пошту, час, випадковість. Value objects і доменні класи беруть справжні. Чужі бібліотеки не мокають напряму: їх загортають у власний вузький порт, домен тестують з фейком цього порту, а сам адаптер integration-тестом проти справжньої бібліотеки чи записаних відповідей.

Чому кажуть «не мокайте те, чим не володієте»? Як тоді тестувати код, що працює з Guzzle чи SDK Stripe?
У тесті сервісу підмінено все, крім самого сервісу. Що з цим не так?
Ви б замокали value object `Money`?
Після оновлення SDK платіжки продакшн упав, а всі тести зелені. Як так вийшло?
test doubles Mockery фейки порти й адаптери контрактні тести

Підміняти варто те, що заважає тесту бути швидким і передбачуваним: базу, мережу, файлову систему, пошту, час і випадковість. Усе, що живе в памʼяті й не має побічних ефектів, краще брати справжнім. Value object Money чи доменна сутність створюються через new за мікросекунди, а мок замість них перевіряє лише власну заглушку: тест з allows('add')->andReturn(...) доводить, що Mockery вміє повертати значення, а не що гроші додаються правильно.

Друге правило походить з книжки Стіва Фрімена (Steve Freeman) і Ната Прайса (Nat Pryce) «Growing Object-Oriented Software, Guided by Tests»: не мокайте типи, якими не володієте. Мок Guzzle чи SDK платіжки фіксує ваше уявлення про те, як вони поводяться. Коли бібліотека після оновлення змінить формат відповіді чи кине інший виняток, мок цього не помітить, і зелений тест буде брехати. Крім того, ланцюжки на кшталт getBody()->getContents() у моках повторюють реалізацію клієнта рядок за рядком.

Замість цього чужий код загортають у власний порт з вузьким інтерфейсом мовою домену: ExchangeRates::usdToUah(), PaymentGateway::charge(). Адаптер у шарі Infrastructure знає про HTTP і JSON, а сценарій лише про порт. Тоді тести домену отримують простий фейк порту, а адаптер перевіряють окремо: у Laravel через Http::fake() з записаними відповідями справжнього API і Http::preventStrayRequests(), у Symfony через MockHttpClient.

Який дублер узяти для власного порту, залежить від того, чи тримає він стан. Репозиторій, кеш, черга: сценарій пише в них і читає, тож fake на масиві чесніший і стійкіший за mock, де треба заздалегідь описати кожен виклик. Порт без стану, як пошта чи шина подій, закривається spy: важливий сам факт виклику. Модель Eloquent і query builder не мокають узагалі: код, що будує запити, тестують на справжній тестовій базі.

Фейк має свою ціну: він може розійтися зі справжнім адаптером. Eloquent-репозиторій сортує за датою, а масив віддає в порядку додавання, і сотні тестів спираються на поведінку, якої в продакшні немає. Від цього рятує контрактний тест, один набір перевірок, який проганяють і на фейку, і на адаптері з базою. Змінилась поведінка порту, змінився контракт, і він сам покаже, яка реалізація відстала.

// Порт мовою домену: про Guzzle і JSON сценарій нічого не знає.
interface ExchangeRates
{
    public function usdToUah(DateTimeImmutable $day): Money;
}

// Фейк порту в tests/Support: проста робоча реалізація.
final class FixedExchangeRates implements ExchangeRates
{
    public function __construct(private Money $rate) {}

    public function usdToUah(DateTimeImmutable $day): Money
    {
        return $this->rate;
    }
}

it('converts a salary to hryvnias', function () {
    $rates = new FixedExchangeRates(Money::uah(41_50));        // курс 41.50 у копійках
    $salary = new ConvertSalary($rates);

    $uah = $salary(Money::usd(5000_00), new DateTimeImmutable('2026-10-01'));

    expect($uah)->toEqual(Money::uah(207_500_00));              // справжній Money, не мок
});

// Адаптер окремо: справжній HTTP-клієнт Laravel, записана відповідь НБУ.
it('reads the NBU rate', function () {
    Http::preventStrayRequests();
    Http::fake(['bank.gov.ua/*' => Http::response([['cc' => 'USD', 'rate' => 41.5]])]);

    $rate = app(NbuExchangeRates::class)->usdToUah(new DateTimeImmutable('2026-10-01'));

    expect($rate)->toEqual(Money::uah(41_50));
});
Критерій підміни: залежність повільна, недетермінована, має побічні ефекти або недоступна в тесті. Не «усе, крім класу під тестом».
Правило «don't mock what you don't own» з книжки Фрімена й Прайса «Growing Object-Oriented Software, Guided by Tests» і пояснення: мок фіксує ваше припущення про чужий API, і зміну бібліотеки тест не помітить.
Рецепт для чужого коду: власний порт мовою домену (`PaymentGateway`, `ExchangeRates`), адаптер у Infrastructure, фейк порту для тестів домену, integration-тест адаптера.
Що для порту зі станом (репозиторій, кеш) fake кращий за mock, а для порту без стану (лист, подія) достатньо spy.
Контрактний тест: один набір перевірок, який ганяють і на фейку, і на справжньому адаптері, щоб фейк не розійшовся з продакшном.
Мокати value object чи сутність: `Mockery::mock(Money::class)` з `allows('add')` замість `new Money(...)`. Тест перевіряє заглушку, а не арифметику, і `final readonly class` такий мок і не дозволить.
Мокати Guzzle `ClientInterface` чи `ResponseInterface` з ланцюжком `getBody()->getContents()`: тест повторює реалізацію клієнта й мовчить, коли API змінить формат.
Мокати Eloquent-модель чи query builder (`shouldReceive('where->first')`): ламається від будь-якої зміни запиту й не перевіряє SQL. Такий код тестують на справжній тестовій базі.
Писати фейк, у якому є бізнес-логіка, якої немає в адаптері: тести зелені, бо фейк робить чужу роботу.
Тримати фейк статичним або синглтоном, і дані з одного тесту протікають в інший.
ПОРАДА

Відповідайте трьома шарами: домен тестую справжніми обʼєктами, межі (база, HTTP, пошта, час) підміняю фейками власних портів, а адаптери до чужого коду перевіряю окремо проти справжньої бібліотеки. І додайте про контрактний тест: без нього фейк рано чи пізно почне брехати.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Мок чужого API фіксує ваше припущення про нього, і зміну бібліотеки тест не побачить. Власний вузький порт дає місце для фейка, а адаптер перевіряється там, де він говорить зі справжньою бібліотекою. Мокати все підряд означає повторювати реалізацію, а value objects дешево й надійно брати справжніми.