Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Події
Інше
Про нас
Новини 12 серпня 2026

Бібліотека Double для мокування PHP-класів у тестах

Jason McCreary, творець Laravel Shift, представив Double - бібліотеку для створення тестових дублів у PHP. Вона замінює традиційний вибір між моком, шпигуном та частковим моком єдиним типом об'єкта: ви створюєте дубль для класу чи інтерфейсу, а дієслова, які використовуєте далі, визначають його поведінку. Double вимагає PHP 8.3 і на момент написання має версію v0.4.0.

Ключові можливості

Бібліотека пропонує наступний функціонал:

  • Один конструктор - Double::for() приймає класи, інтерфейси, кілька інтерфейсів одночасно або реальний екземпляр
  • Три режими - loose (за замовчуванням) повертає типобезпечні значення для неналаштованих викликів, strict викидає виняток, а passthru делегує виклики реальному об'єкту
  • Два дієслова налаштування - expects() для виклику, який обов'язково має статися, allows() для необов'язкового
  • Перевірки у стилі шпигуна на кожному дублі - received() дозволяє перевірити, що сталося, без попереднього оголошення шпигуна
  • Матчери аргументів - Argument::any(), type(), same(), matches(), contains(), capture(), not() та remaining()
  • Повідомлення про помилки з логом викликів - невиконане очікування показує, з якими аргументами метод насправді викликався
  • Інтеграція з PHPUnit - помилки звітуються як failures замість errors, успішні перевірки враховуються як assertions, а трейт автоматично верифікує кожен дубль
  • Мапінг з Mockery - документація включає довідник методів для конвертації існуючого набору тестів

Порівняння з Mockery

Якщо ви переходите з Mockery, ось той самий тест, написаний обома способами. Спочатку в Mockery:

use Mockery;
$repository = Mockery::spy(BookRepository::class);
$repository->shouldReceive('find')->once()->with(123)->andReturn($book);
$service = new CatalogService($repository);
$service->lookup(123);
$repository->shouldHaveReceived('recordView')->with($book);
Mockery::close();

Той самий тест у Double:

use JMac\Testing\Double;
$repository = Double::for(BookRepository::class);
$repository->expects('find')->with(123)->returns($book);
$service = new CatalogService($repository);
$service->lookup(123);
$repository->received('recordView')->with($book);

Чотири зміни:

  • Немає рішення mock() чи spy() на початку, оскільки received() працює на будь-якому дублі
  • once() прибрано, бо це вже означає expects()
  • shouldReceive() та andReturn() стають expects() та returns(). Double зберігає одне дієслово на концепцію без аліасів
  • Mockery::close() не має аналога. Викличте $repository->verify() або додайте трейт VerifiesDoubles і дозвольте йому спрацювати автоматично

Вивід помилок також відрізняється. Якщо код викликає find('Baz') замість find('baz'), Mockery звітує:

Method find('baz') from Mockery_0_BookRepository should be called
exactly 1 times but called 0 times.

Double звітує:

Double `foo` expected `find('baz')` to be called exactly 1 time, but it was never called.
The following calls to `find` were made during this test: `find('Baz')`

Double називає клас, який ви дублювали, замість згенерованого Mockery_0_ ідентифікатора, а другий рядок перелічує, що метод насправді отримав.

Створення дубля та вибір режиму

Double::for() повертає реальний об'єкт, який задовольняє instanceof та будь-яку type hint, що очікує ціль:

use JMac\Testing\Double;
$repository = Double::for(BookRepository::class);
$service = new CatalogService($repository);

Передайте більше однієї цілі, і ви отримаєте єдиний дубль, що реалізує всі з них. Все після першого має бути інтерфейсом - те саме правило, що PHP застосовує до intersection types:

$logger = Double::for(LoggerInterface::class, FlushableInterface::class);

Кожен дубль має рівно один режим, який контролює, що станеться, коли виклик не відповідає нічому з налаштованого. Loose mode (за замовчуванням) повертає типобезпечне значення на основі оголошеного типу повернення методу: false для bool, 0 для int, [] для array, перший case enum'у, сам дубль для self. Для non-nullable типу класу чи інтерфейсу генерується свіжий дубль цього типу замість повернення null. Генерація працює лише на один рівень глибини.

Strict mode падає на першому неналаштованому виклику. Passthru делегує неналаштовані виклики реальному екземпляру і все ще записує кожен виклик:

$repository = Double::for(BookRepository::class)->strict();
$logger = Double::for(Logger::class)->passthru($realLogger);

Конфігурація живе на самому дублі, тому сім назв методів зарезервовано: expects, allows, strict, passthru, received, unused та verify. Дублювання класу, що оголошує один з них, негайно викидає виняток.

Очікування та зіставлення аргументів

Налаштування дубля читається зліва направо, і кожен модифікатор має значення за замовчуванням:

$repository->expects('find')->with(123)->returns($book);
$repository->allows('find')->with(999)->throws(new NotFoundException());
$repository->allows('calculateTax')->resolves(fn (...$args) => $gateway->calculateTax(...$args));

expects() означає, що виклик має статися рівно один раз, якщо не вказано інше. allows() означає, що він може статися будь-яку кількість разів, включаючи нуль.

Підрахунок викликів працює інакше, ніж у Mockery. Double направляє все через одне дієслово, використовуючи іменовані аргументи:

$repository->expects('save');                      // once, за замовчуванням
$repository->expects('save')->times(2);            // двічі
$repository->expects('save')->times(5);            // рівно 5
$repository->expects('save')->times(1, 3);         // між 1 та 3
$repository->expects('save')->times(minimum: 2);   // щонайменше 2
$repository->allows('save')->times(maximum: 5);    // щонайбільше 5
$repository->allows('save')->never();              // скорочення для times(0)

Ті самі підрахунки працюють на received() після факту:

$repository->received('save')->with($book)->times(2);
$repository->received('delete')->never();

Передача кількох значень у returns() будує чергу. Коли два очікування можуть відповідати одному виклику, перемагає останнє зареєстроване.

Просте значення в with() порівнює скаляри та масиви через ===, а об'єкти через ==. Все менш строге йде через фасад Argument:

use JMac\Testing\Matching\Argument;
$repository->allows('save')->with(Argument::type(Book::class))->returns(true);
$repository->allows('find')->with(Argument::any(1, 2, 3))->returns($book);
$repository->allows('find')->with(Argument::matches('/^\d+$/'))->returns($book);
$repository->allows('saveAll')->with(Argument::contains($book))->returns(true);
$repository->allows('combine')->with('-', Argument::remaining())->returns('a-b-c');

Argument::same() перевіряє ідентичність, де простий об'єкт лише перевіряє еквівалентність. Argument::capture($var) підходить до чогось завгодно і записує реальний аргумент у змінну для подальших assertions.

Коли порядок викликів є частиною контракту, позначте відповідні очікування ordered():

$connection->expects('open')->ordered();
$connection->expects('write')->ordered();
$connection->expects('close')->ordered();

Верифікація

verify() перевіряє кожне зареєстроване expects() і є еквівалентом Mockery::close(). received() йде в іншому напрямку, перевіряючи після факту, і доступний на кожному дублі незалежно від способу створення.

Також є unused(), який стверджує, що дубль не отримав жодних викликів до будь-якого методу:

Double `Logger` expected no calls at all, but received: `info('something happened')`.

Повідомлення про помилки

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

Can't configure `sav` on a double for `BookRepository`. That method
does not exist. Did you mean `save`?

У strict mode неналаштований виклик пропонує рядок allows(), який це виправить:

Double `foo` received an unexpected call to `bar(1, 2)`. Strict mode
requires every call to be configured. For example:
`$foo->allows('bar')->returns(...)`.

Інтеграція з PHPUnit

Нічого в бібліотеці не вимагає PHPUnit, але три речі змінюються, коли він встановлений. Помилки розширюють AssertionFailedError PHPUnit, тому невиконане очікування звітується як failure, а не error. Успішні верифікації реєструють реальний assertion. А трейт усуває ручний виклик verify():

use JMac\Testing\Integrations\PHPUnit\VerifiesDoubles;
class TestCase extends \PHPUnit\Framework\TestCase
{
    use VerifiesDoubles;
}

Кожен дубль, створений під час тесту, і кожен assertion received(), зроблений на ньому, перевіряється, коли тест завершується. Це працює з PHPUnit 11 та 12.

Конвертація існуючого набору тестів

Документація включає мапінг метод-за-методом. Найпоширеніші рядки:

  • Mockery::mock(Foo::class)Double::for(Foo::class)
  • Mockery::spy(Foo::class)Double::for(Foo::class), потім received()
  • shouldIgnoreMissing() → за замовчуванням
  • shouldDeferMissing()passthru($realInstance)
  • shouldReceive('foo')->once()->andReturn($x)expects('foo')->returns($x)
  • shouldReceive('foo')->andReturn($x)allows('foo')->returns($x)
  • andThrow($e) / andReturnUsing($fn)throws($e) / resolves($fn)
  • shouldHaveReceived('foo')received('foo')
  • Mockery::close()verify() або трейт VerifiesDoubles

Таблиця матчерів має таку саму форму. Кілька функцій Mockery навмисно відсутні, включаючи аліаси, ducktype(), мокування статичних методів та byDefault(). Також доступний безкоштовний Double Converter, який автоматизує конвертацію з Mockery на Double.

Встановлення

Встановіть пакет як dev-залежність:

composer require --dev jasonmccreary/double

Немає service provider і конфігураційного файлу, тому бібліотека працює в будь-якому наборі тестів на PHPUnit або Pest, всередині Laravel чи ні. Щоб дублювати final клас, викличте Double::bypassFinals() як перший рядок вашого bootstrap-файлу PHPUnit.

Повна документація доступна на testdoublephp.com, а сам Double можна знайти на GitHub.

7

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

whereBinary()
Новини 28 серпня 2026

whereBinary(): регістрозалежні запити MySQL у Laravel

Laravel 13.27 додає новий метод whereBinary() для точного побайтового порівняння рядків у MySQL. Він вирішує проблему, коли стандартне collation utf8mb4_unicode_ci ігнорує регістр, акценти та пробіли при порівнянні токенів, slug-ів та інших критичних даних.

ToolSearch
Новини 27 серпня 2026

Laravel AI: завантаження інструментів на вимогу з ToolSearch

Laravel AI v0.11.0 додає механізм відкладеного завантаження інструментів агента через ToolSearch. Замість надсилання всіх тридцяти інструментів одразу, провайдер отримує лише пошуковий запис і завантажує повні визначення тільки тоді, коли модель вирішує їх використати.

2

Вакансії за темою

Alliance Digital Нова
Сьогодні

PHP Developer

Middle PHP-розробник для фінтех-продукту. Розробка backend на Laravel з PostgreSQL, RabbitMQ, Redis. Реалізація складної бізнес-логіки кредитування, фінансових операцій, зовнішніх інтеграцій. Вимоги: 3+ років PHP, впевнена робота з Laravel, OOP/SOLID, складна бізнес-логіка, PostgreSQL, черги, тестування, production-mindset.

N-iX
16 днів тому

Senior PHP Engineer (#5311)

Senior PHP Engineer для консолідації двох legacy портальів в єдину сучасну платформу. Основний стек: PHP 8.x, Laravel, RabbitMQ, MySQL, Nuxt.js/Vue.js на AWS. Потрібні 5+ років досвіду з PHP, deep expertise в Laravel, event-driven архітектурі, AWS та SQL optimization. Роль передбачає дизайн високоякісного коду, архітектурні обговорення, роботу по всьому стеку та співпрацю з розподіленою командою в різних часових поясах.

IT Dream Service
20 днів тому

Full-Stack Developer (PHP Laravel + Vue 3)

Full-Stack розробник для внутрішної B2B-платформи управління задачами. Розробка backend на Laravel 10/11 та frontend на Vue 3 + Inertia.js, проектування MySQL, тестування (PHPUnit/Vitest), робота з дизайн-системою Tailwind. Вимога: 3+ років досвіду з Laravel та Vue 3, знання Inertia.js/SPA, MySQL, REST API, Git-флоу, SOLID.

Пакети за темою

Pest

pestphp/pest

Тестовий фреймворк з лаконічним синтаксисом поверх PHPUnit: тести описуються функціями замість класів. Має паралельний запуск, тести архітектури, покриття і мутаційне тестування.

11,667 v5.1.1 2

Bagisto

bagisto/bagisto

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

27,987 v2.4.10 12 21