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

Чим mock відрізняється від stub, spy і fake?

Усі вони test doubles, обʼєкти, що в тесті замінюють справжню залежність. Dummy лише заповнює параметр, stub повертає заготовлені відповіді, spy ще й записує виклики для перевірки після дії, mock отримує очікування до дії й сам падає, якщо їх не виконано, fake це робоча спрощена реалізація, як репозиторій на масиві. Stub і fake ведуть до перевірки стану, spy і mock до перевірки поведінки.

Що таке test double і які вони бувають?
Ви написали `Mockery::mock()` і не додали жодного `expects()`. Це мок чи ні?
`Mail::fake()` у Laravel це fake чи spy?
Чому тест з моком упав після рефакторингу, хоча сторінка працює як раніше?
test doubles mock stub spy fake Mockery

Test double це загальна назва для обʼєкта, що в тесті стає на місце справжньої залежності й має той самий інтерфейс. Назви ролей походять з книжки Джерарда Месароша (Gerard Meszaros) «xUnit Test Patterns» 2007 року, а широко відомими їх зробила стаття Мартіна Фаулера (Martin Fowler) «Mocks Aren't Stubs». На співбесіді чекають саме цих пʼяти ролей, бо плутанина між ними і є головною причиною крихких тестів.

Dummy лише заповнює параметр: конструктор вимагає логер, але сценарій до нього не доходить. Stub повертає заготовлені відповіді й нічого не памʼятає: existsByEmail() завжди false. Spy це stub, який ще й записує, як його викликали, а тест читає ці записи після дії. Mock отримує очікування до дії («рівно один виклик send() з такими аргументами») і сам валить тест, якщо їх не виконано або прийшов зайвий виклик. Fake це робоча, але спрощена реалізація: репозиторій на масиві, що справді памʼятає збережене, або поштова скринька в памʼяті.

Головна різниця в тому, що саме перевіряє тест. Перевірка стану дивиться на результат: що повернув метод і що лежить у фейковому репозиторії. Перевірка поведінки дивиться на виклики залежностей. Stub і fake ведуть до першої, spy і mock до другої. Перевірка стану переживає рефакторинг: якщо save() замінили на saveMany(), користувач однаково лежить у репозиторії. Перевірка викликів знає, як код працює всередині, і падає від такої заміни, хоча поведінка не змінилась.

Звідси практичне правило: перевіряйте стан, коли можете, а виклики лише там, де дія не лишає сліду, який можна прочитати, як лист, подія чи запит до чужого API. Для запитів-читань ставте stub, а не очікування: питання «скільки разів код викликав find()» зазвичай нікого не цікавить. Між spy і mock різниця в порядку: зі spy тест іде звично (підготували, зробили, перевірили), mock строгіший і ловить неочікувані виклики одразу.

Інструмент не визначає роль. Mockery::mock() з одним allows() працює як stub, той самий обʼєкт без жодного дозволеного методу як dummy, що впаде, якщо сценарій його все ж торкнеться. Фейки Laravel (Mail::fake(), Queue::fake()) за класифікацією Месароша ближчі до spy: вони записують відправлене, а тест перевіряє його після дії. У PHPUnit для stub є createStub(), для mock createMock() з expects(), і починаючи з PHPUnit 12.5 мок без очікувань дає notice з порадою взяти stub.

// Registration: перевіряє адресу в UserRepository, зберігає, шле лист через Mailer.

it('rejects a taken email', function () {
    $users = Mockery::mock(UserRepository::class);
    $users->allows('existsByEmail')->andReturn(true);   // stub: лише відповідь
    $mailer = Mockery::mock(Mailer::class);              // dummy: до send() не дійде

    expect(fn () => (new Registration($users, $mailer))->register('olena@example.com'))
        ->toThrow(DomainException::class);
});

it('sends a welcome email', function () {
    $users = new InMemoryUsers;                          // fake: справжня логіка в масиві
    $mailer = Mockery::spy(Mailer::class);               // spy: записує всі виклики

    (new Registration($users, $mailer))->register('olena@example.com');

    expect($users->existsByEmail('olena@example.com'))->toBeTrue();   // стан
    $mailer->shouldHaveReceived('send')                                // поведінка
        ->once()
        ->with('olena@example.com', 'Вітаємо на PHP Ukraine');
});

it('sends a welcome email, mock style', function () {
    $mailer = Mockery::mock(Mailer::class);
    $mailer->expects('send')                             // mock: очікування до дії,
        ->with('olena@example.com', 'Вітаємо на PHP Ukraine'); // рівно один виклик

    (new Registration(new InMemoryUsers, $mailer))->register('olena@example.com');
});
Пʼять ролей з точним визначенням кожної, а не «мок це будь-яка підробка»: dummy, stub, spy, mock, fake.
Різниця між перевіркою стану (що повернув код і що лишилось) і перевіркою поведінки (які виклики відбулися), і що stub з fake належать до першої, а spy з mock до другої.
Що spy і mock перевіряють те саме, але в різний момент: spy після дії (`shouldHaveReceived()`), mock до неї (`expects()`), тому mock падає вже на неочікуваному виклику.
Звідки слова: книжка Джерарда Месароша «xUnit Test Patterns» (2007) і стаття Мартіна Фаулера «Mocks Aren't Stubs».
Що назва інструмента не визначає роль: `Mockery::mock()` з одним `allows()` працює як stub, а `Mail::fake()` за класифікацією Месароша ближчий до spy, бо записує виклики й перевіряється після дії.
Називати моком будь-який дублер і не вміти пояснити, чим stub відрізняється від mock.
Перевіряти виклик там, де можна перевірити результат: `expects('save')->once()` замість того, щоб подивитися, що лежить у репозиторії. Тест ламається, щойно `save()` замінять на `saveMany()`.
Ставити очікування на stub-виклик: `expects('existsByEmail')->andReturn(false)` робить з запиту, який нічого не змінює, перевірку поведінки, і тест падає від кешування чи зайвого виклику.
Вважати fake «моком, який написали руками»: fake має справжню логіку (памʼятає збережене, видає id), а не набір заготовлених відповідей.
ПОРАДА

Скажіть одне речення про кожну роль на одному прикладі, скажімо реєстрації: dummy-логер, якого сценарій не торкається; stub репозиторію, що відповідає «адреса вільна»; spy пошти, у якому після дії перевіряємо лист; mock пошти з очікуванням до дії; fake-репозиторій на масиві. Потім додайте правило: стан перевіряю завжди, коли можу, виклики лише для дій без сліду в стані.

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

Spy записує всі виклики, і тест питає про них після дії (`shouldHaveReceived()`); mock отримує очікування заздалегідь (`expects()`) і падає на неочікуваному виклику. Stub лише відповідає, mock перевіряє виклики, а не навпаки. Fake має справжню спрощену логіку, а не заготовлені відповіді. Роль визначає використання: `Mockery::mock()` з одним `allows()` працює як stub.