vyvchy
    Теми розділу

    06 · Автоматизація: стратегія

    Тестові дублери: stub, mock, fake, spy

    Зміст

    Автотест рідко перевіряє код у вакуумі: майже завжди навколо перевіреного шматка є залежності (dependencies) — база даних, платіжний , поштовий сервіс, чуже API. І кожна така залежність тягне за собою три біди: вона повільна, вона недетермінована, і вона має справжні побічні ефекти. Реальний платіж списує реальні гроші, реальна пошта летить реальному користувачу, а сторонній сервіс сьогодні відповідає за 200 мілісекунд, а завтра лягає — і твій зелений тест раптом червоніє не через баг застосунку. Щоб перевіряти логіку, а не чужу інфраструктуру, залежність підміняють підставним об'єктом — (test double). Назва — з кіно: дублер (stunt double) робить трюк замість , а глядач бачить актора.

    Ця глава — канонічний довідник таксономії дублерів для всього сайту: інші розділи посилаються сюди по означення stub, mock, spy і fake. При першому проході її спокійно можна пропустити й повернутися пізніше — вона щільно спирається на піраміду тестування й анатомію автотесту, тож має сенс читати її, коли ті вже вкладені в голову. Головна складність теми не технічна, а термінологічна: слова mock і stub вживають як синоніми навіть досвідчені інженери.

    Уявімо перевірку: «якщо платіжний шлюз відхилив картку, замовлення не створюється, а користувач бачить помилку». Щоб перевірити цю гілку на живому шлюзі, треба якимось чином змусити його відповісти «відхилено» — а він на тестову картку відповідає «успіх». Негативний сценарій просто недосяжний руками. Ось де дублер незамінний: він дає нам керований вхід — ми програмуємо відповідь «declined» і дивимось, чи правильно застосунок її обробляє.

    Дублер розв'язує чотири проблеми одразу:

    • Швидкість. Виклик у пам'яті замість походу в мережу чи БД — мілісекунди замість секунд.
    • Детермінованість. Дублер повертає те саме щоразу; зовнішній сервіс — ні. Це прямий засіб проти флакі-тестів.
    • Досяжність . Помилку 500, порожній список, , «картку відхилено» легко змоделювати дублером і майже неможливо — реальним сервісом на вимогу.
    • Відсутність побічних ефектів. Жодного реального списання, жодного реального листа.

    Але за ізоляцію платимо. Що більше ми підмінили, то далі тест від реальності — і то більший , що дублер розійдеться зі справжньою залежністю (до цього повернемось у розділі про контрактні тести). Тому дублер — не безкоштовний і не завжди правильний вибір: це компроміс між достовірністю й контролем.

    Таксономія: dummy, stub, spy, mock, fake

    Канонічний поділ увів Джерард Мезарош (Gerard Meszaros) у книзі «xUnit Test Patterns». «Test double» — це збірний термін, а під ним п'ять різновидів. Розрізняє їх не синтаксис, а роль у тесті: одні дають системі під тестом (system under test, SUT) вхідні дані, інші існують, щоб перевірити її вихід, а один взагалі нічого не робить.

    • Dummy — порожній об'єкт, який передають лише щоб заповнити параметр; його ніколи по-справжньому не викликають. Типовий приклад — null чи порожній об'єкт, потрібний тільки для того, щоб сигнатура методу зійшлася.
    • Stub — повертає наперед задані відповіді на виклики. Це джерело керованого входу для SUT: платіжного клієнта завжди каже declined, стаб репозиторія завжди повертає того самого користувача. Стаб байдужий до того, як його викликали, — він просто віддає своє.
    • Spy — це стаб, який ще й записує, як його викликали: скільки разів, з якими аргументами. Перевірку робиш ти сам після дії: « зафіксував рівно один виклик sendEmail з правильною адресою?».
    • Mock — об'єкт із вбудованими очікуваннями (expectations): він наперед знає, як його мають викликати, і якщо очікування не справдилось — тест валить сам, без окремого expect. По суті — це специфікація взаємодії, вшита в об'єкт.
    • Fake — має справжню, робочу реалізацію, але спрощену й непридатну для продакшену. Класика — база в пам'яті замість Postgres, in-memory репозиторій, підроблений SMTP-сервер. Fake реально «працює», просто зрізає кути (не персистить на диск, не масштабується).

    Test double
    підставний об'єкт

    Дають вхід SUT

    Перевіряють вихід SUT

    Не використовуються

    Stub:
    наперед задана відповідь

    Fake:
    спрощена робоча реалізація

    Spy:
    записує виклики

    Mock:
    очікування, сам фейлить

    Dummy:
    заповнює параметр

    Test double
    підставний об'єкт

    Дають вхід SUT

    Перевіряють вихід SUT

    Не використовуються

    Stub:
    наперед задана відповідь

    Fake:
    спрощена робоча реалізація

    Spy:
    записує виклики

    Mock:
    очікування, сам фейлить

    Dummy:
    заповнює параметр

    Важлива практична заувага: у світі JS/TS цю термінологію майже ніхто не тримає строго. Бібліотека sinon чесно розрізняє spy / stub / mock, а от jest.fn() і jest.mock() змішують усе під словом «мок». На рівні браузера page.route(...).fulfill(...) у Playwright — це, за таксономією, мережевий стаб, хоча всі кажуть «замокав запит». Тому на співбесіді корисно вміти назвати річ точно, навіть коли команда навколо зве її «моком».

    Mock проти stub коротко

    Це найчастіше питання теми, і відповідь тримається на одній осі — що саме ти перевіряєш:

    • Stub → перевірка стану (state verification). Ти годуєш SUT відповіддю й далі перевіряєш результат чи стан самої SUT. Якщо SUT з якоїсь причини взагалі не звернеться до стаба — тест усе одно пройде: стабу байдуже.
    • Mock → перевірка поведінки (behavior verification). Ти перевіряєш не результат, а сам факт взаємодії: «метод charge викликали рівно один раз із сумою 100?». Мок несе очікування в собі й падає, якщо виклику не сталося.
    АспектStubMock
    Що перевіряєСтан / результат SUTФакт і параметри виклику
    Хто робить перевіркуТи, після дії (expect)Мок сам, за вбудованим очікуванням
    Якщо виклику не сталосяТест зеленийТест червоний
    Тип відповідіДає вхід у SUTПеревіряє вихід SUT

    Практичне правило: стаб — коли важлива відповідь, що повертається; mock або spy — коли важливий сам факт виклику (тобто побічний ефект). Перевіряєш, що з відхиленої картки не створилось замовлення, — тобі потрібен стаб відповіді. Перевіряєш, що після реєстрації пішов лист, — тобі потрібен spy чи mock на відправнику.

    // Stub: годуємо SUT відповіддю, перевіряємо стан ПІСЛЯ
    const gateway = { charge: async () => ({ status: 'declined' }) };
    const result = await checkout(cart, gateway);
    expect(result.orderCreated).toBe(false); // перевірка стану SUT
    
    // Spy: записує виклики, перевірка — вручну після дії
    const sendEmail = sinon.spy();
    await register(user, sendEmail);
    expect(sendEmail.calledOnceWith(user.email)).toBe(true); // перевірка поведінки

    Дублери на рівнях піраміди

    Кількість і тип дублерів залежать від рівня, на якому ти тестуєш. Загальний закон простий: що вище піраміда, то менше дублерів і більше справжнього.

    • Unit. Дублерів найбільше: усе навколо перевіреної функції підмінене. Працюють на рівні коду, у процесі — jest, sinon, vitest. Тут живуть класичні stub/spy/mock у чистому вигляді.
    • Integration / component / API. Дублерів менше: справжню взаємодію лишаємо, підмінюємо лише зовнішні межі. Тут дублер — це вже не об'єкт у коді, а мережевий стаб: msw, nock, WireMock, mock-сервер зі специфікації OpenAPI. Детально механіку сервісного мокання розбирає розділ API-тестування.
    • E2E. Дублерів найменше: в ідеалі справжній фронтенд, справжній бекенд, справжня БД. Але навіть тут окремі межі підмінюють — через браузерне (перехоплення й мокання мережі), наприклад page.route у Playwright.

    Це прямо продовжує правило найнижчого рівня з глави про піраміду: логіку перевіряй якнайнижче з максимумом дублерів, а нагорі лиши мінімум перевірок на справжній системі.

    Реальний бекенд чи мок

    На рівні e2e є вічний холівар: ганяти тести проти справжнього бекенда чи проти замоканого. У кожного підходу своя ціна.

    Справжній бекендЗамокані відповіді
    ДостовірністьВисока: ловить інтеграційні й контрактні багиНизька: перевіряє «вигадану» відповідь
    Швидкість / стабільністьПовільніше, флакішеШвидко, детерміновано
    Edge cases (500, порожньо, повільно)Важко відтворитиФорсуються тривіально
    РизикТест залежить від стенду й данихТест може розійтися з реальним API

    Правильна відповідь — не «або-або», а шарами. Критичний наскрізний шлях (, логін, оплата успіхом) варто хоч кількома тестами прогнати проти справжнього бекенда — інакше ти ніколи не зловиш баг інтеграції. А десятки варіацій відображення й обробки помилок дешевше й надійніше ганяти на замоканих відповідях, де кожен 500 чи порожній список даються одним рядком.

    Орієнтир, який рятує від крайнощів: мокай те, чого не контролюєш, і те, що не можеш зробити детермінованим; лишай справжнім те, що власне хочеш перевірити.

    Що мокають навіть в e2e: платежі, пошта, сторонні API

    Навіть у найчеснішому «повністю справжньому» e2e є речі, які підмінюють майже завжди — бо в них або незворотний побічний ефект, або чужа неконтрольована інфраструктура.

    • Платежі. Прогнати справжнє списання Visa на кожному прогоні CI неможливо. Використовують провайдера (наприклад, тестові картки Stripe — 4242 4242 4242 4242 для успіху й окремі номери, що форсують ) або власний . Сценарій «картку відхилено» — це просто стаб відповіді шлюзу.
    • Пошта й SMS. Реальні листи не шлють. Ставлять перехоплювач — MailHog чи Mailtrap: тест забирає останнє повідомлення, перевіряє його вміст і навіть витягує посилання підтвердження, щоб піти по ньому далі. По суті це fake-поштовик плюс spy на тому, що застосунок надіслав.
    • Сторонні API. Геолокація, погода, KYC-перевірки, аналітика, соціальний логін. Їхній аптайм, дані й ліміти запитів — не твої; якщо не підмінити, їхня нестабільність стає твоєю. Їх стаблять на мережевій межі.
    • Час і випадковість. Детермінований годинник (у Playwright — page.clock) і (seed) для генератора випадкових чисел прибирають цілий клас «тест падає лише опівночі».
    // Форсуємо «картку відхилено» — стаб відповіді платіжного шлюзу в e2e
    await page.route('**/api/payments', (route) =>
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ status: 'declined', reason: 'insufficient_funds' }),
      }),
    );

    Спільний знаменник усього списку: підмінюємо межі поза контролем команди й дії з незворотним ефектом у реальному світі (гроші, повідомлення). Решту лишаємо справжнім.

    Контрактні тести як страховка

    У мокання є одна підступна вада, про яку варто сказати окремо. Стаб — це заморожена копія того, як API відповідав колись. Бекенд перейменував поле status на state — а твій стаб і далі віддає старе status, тест лишається зеленим і бреше. Ми називаємо це розсинхроном схеми (schema drift) — і одразу чесно: це наша робоча назва, а не галузевий стандарт. Саме явище джерелами описане добре (мок старіє, реалізація розходиться зі специфікацією), але усталеної англійської назви для нього в жодному з наших джерел немає, тож не варто чекати, що співрозмовник на співбесіді впізнає термін. Чим більше моканих меж, то більший ризик, що зелена перевіряє вигадку.

    Страховка від цього — (contract testing). Контракт — це зафіксована домовленість про форму взаємодії між (consumer) і постачальником (provider). Ідея consumer-driven contracts (наприклад, Pact): очікування споживача — по суті його стаб — записуються як контракт, а CI постачальника програє цей контракт проти справжнього сервісу й доводить, що той досі його дотримується. Полегшений варіант тієї самої ідеї — валідувати реальні відповіді проти схеми OpenAPI.

    Головна думка оглядово: контракт дає тобі швидкість моканих тестів без їхньої мовчазної брехні — щойно постачальник ламає форму, падає контрактний тест, а не прод. Механіку контрактів детально розбирає розділ API-тестування, а роль контрактів у розподіленій системі — глава тестування мікросервісів.

    Типові помилки

    • Виглядає як «замокав — зелено — значить, працює», а насправді ти протестував свій мок. Overmocking: , де підмінено все навколо, доводить лише те, що твої дублери викликались так, як ти сам їх налаштував. Логіки застосунку він при цьому може не перевіряти взагалі.
    • Виглядає як mock, а насправді stub. Люди звуть моком усе підряд, бо бібліотека зветься jest.mock. Якщо ти лише повертаєш наперед задану відповідь і перевіряєш стан SUT — це стаб, хоч як його називає API бібліотеки.
    • Виглядає як «мок ловить регресію API», а насправді ні. Мок заморожений на старій формі відповіді; коли бекенд змінює контракт, тест лишається зеленим (drift). Ловить це не мок, а контрактний тест.
    • Виглядає як мок, а насправді нікчемний spy. Поставив дублер, задав очікування, але забув його перевірити — і виклику могло не статись. Мок без верифікації expectation безсенсовний: він мовчки пропускає баг.
    • Виглядає як «page.route зробив із e2e модульний тест», а насправді ні. Мережевий стаб на межі не перетворює на модульний тест — він лише прибирає одну неконтрольовану залежність; браузер, рендеринг і решта шляху лишаються справжніми.

    Підсумок

    • Test double — збірний термін; за Мезарошем п'ять різновидів: dummy, stub, spy, mock, fake. Розрізняє їх роль у тесті, а не синтаксис.
    • Головна вісь — stub проти mock: стаб годує SUT відповіддю (перевірка стану), mock і spy перевіряють сам факт виклику (перевірка поведінки).
    • Що вище піраміда, то менше дублерів і більше справжнього; на межах, які не контролюєш, дублер стоїть завжди.
    • Навіть у e2e підмінюють незворотні й непідконтрольні межі: платежі, пошту, сторонні API, час.
    • Мок може розійтися з реальністю (drift) — від цього страхують контрактні тести, а не самі моки.

    Можливі питання

    • «Чим mock відрізняється від stub?» — головне питання теми. Інтерв'юер хоче почути не завчену дефініцію, а вісь «перевірка стану проти перевірки поведінки»: стаб дає вхід, мок верифікує взаємодію й падає сам.
    • «Назви типи тестових дублерів.» — очікують п'ять за Мезарошем і бодай по фразі про кожен; плюс бонус, якщо чесно скажеш, що в JS-бібліотеках межі розмиті.
    • «Що б ти мокав в e2e, а що ні?» — перевіряють судження, а не список. Сильна відповідь: мокаю незворотне й непідконтрольне (платежі, пошта, сторонні API, час), лишаю справжнім те, що власне перевіряю.
    • «Чому погано мокати все?» — чекають слова overmocking і думки, що такий тест перевіряє власні моки, а не застосунок, і сліпий до drift.
    • «Як перевіриш сценарій "картку відхилено"?» — дивляться, чи розумієш, що негативний шлях зовнішнього сервісу форсують стабом відповіді або тестовою карткою пісочниці, а не «якось на реальному шлюзі».
    • «Що таке контрактний тест і навіщо?» — тут перевіряють, чи бачиш ти ризик розсинхрону моків із реальністю й чи знаєш, що контракт — це страховка від нього.

    Джерела

    Навіщо ізолюватися

    • xUnit Test Patterns — Test Double — дублер замінює компонент, від якого залежить SUT, «специфічним для тесту еквівалентом», і зобовʼязаний повторити API, а не поведінку; там же нормативна вимога — має бути хоча б один тест без дублерів, бо з дублером тестується інша конфігурація, ніж піде в продакшн.
    • ISTQB® CTFL Syllabus v4.0.1 — як цю саму техніку називає базовий канон: стаби, драйвери, симулятори й — це елементи тестового оточення, окремої таксономії дублерів у CTFL немає.

    Таксономія: dummy, stub, spy, mock, fake

    • xUnit Test Patterns — Test Double — джерело самої пʼятірки: стаб — точка керування непрямими входами, спай — точка спостереження («стаб із записом»), фейк — робоча реалізація зі спрощенням, дамі — заповнювач параметра, який автор строго дублером навіть не вважає; там же омонім Temporary Test Stub.
    • Martin Fowler — Mocks Arenʼt Stubs — межа, яку найчастіше стирають: мок — це не «стаб плюс асерти», а обʼєкт із заздалегідь заданими очікуваннями, що є специфікацією очікуваних викликів.
    • Playwright — Mock APIs — інструментальна реалізація мережевого стаба: повний стаб означає, що запит до API не відбувається взагалі, а окремий режим — виконати справжній запит і пропатчити відповідь.

    Mock проти stub коротко

    • Martin Fowler — Mocks Arenʼt Stubs — власне різниця: перевірка стану по стану залежності, перевірка поведінки — перевірка, що SUT зробив правильні виклики; мок завжди поведінкова, стаб може працювати в обидва боки, а стаб із поведінковою перевіркою — це спай.
    • xUnit Test Patterns — Test Double — коли перевірка відбувається: у спая записане звіряють пізніше, у фазі verification, а мок перевіряє непрямі виходи під час взаємодії.

    Дублери на рівнях піраміди

    • Toby Clemson — Testing Strategies in a Microservice Architecture — механізм, що ставить дублери на рівні: що грубозернистіший тест, то більше в ньому рухомих частин, тож нагору піднімають менше й рідше (trust: secondary, dec-0311).
    • xUnit Test Patterns — Test Double — обмежувач для нижніх рівнів: надмірне вживання дублерів названо причиною через переспецифікацію.

    Реальний бекенд чи мок

    • Martin Fowler — Mocks Arenʼt Stubs — два стилі названо поіменно: класичний бере реальні обʼєкти, поки це зручно, мокістський мокає будь-який обʼєкт із нетривіальною поведінкою; ціна другого — сильніше зчеплення з реалізацією, яке заважає рефакторингу.
    • xUnit Test Patterns — Test Double — і незнімна вимога до будь-якого вибору: хоча б один тест має йти без дублерів.

    Що мокають навіть в e2e: платежі, пошта, сторонні API

    • Playwright — Mock APIs — механіка підміни на рівні мережі: маршрут перехоплюється, відповідь віддається заготовленою, реального запиту не відбувається.
    • xUnit Test Patterns — Test Double — коли фейк доречний: залежність ще не готова, надто повільна або має шкідливі побічні ефекти — це і є платежі та розсилки.
    • ISTQB® CTFL Syllabus v4.0.1 — той самий прийом у словнику канону: віртуалізація сервісів як елемент тестового оточення.

    Контрактні тести як страховка

    • Pact — docs — контрактне тестування як механізм: очікування споживача фіксуються у виконуваному контракті, який перевіряє постачальник.
    • Ian Robinson — Consumer-Driven Contracts — першоджерело самої ідеї: контракт формулює споживач, а постачальник еволюціонує, не ламаючи його.
    • xUnit Test Patterns — Test Double — чому страховка взагалі потрібна: з дублером ми тестуємо іншу конфігурацію, ніж піде в продакшн.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.