---
title: "Бібліотека Double для мокування PHP-класів у тестах"
url: https://laravelukraine.com/blog/biblioteka-double-dlia-mokuvannia-php-klasiv-u-testax
date: 2026-08-12
source: https://laravel-news.com/double-php-testing-library?utm_medium=feed&utm_source=feedpress.me&utm_campaign=Feed%3A+laravelnews
---

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

Jason McCreary, творець [Laravel Shift](https://laravelshift.com/), представив **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:

```php
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:

```php
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, що очікує ціль:

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

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

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

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

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

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

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

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

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

```php
$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 направляє все через одне дієслово, використовуючи іменовані аргументи:

```php
$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()` після факту:

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

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

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

```php
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()`:

```php
$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()`:

```php
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](https://laravelshift.com/mockery-test-double-converter), який автоматизує конвертацію з Mockery на Double.

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

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

```bash
composer require --dev jasonmccreary/double
```

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

Повна документація доступна на [testdoublephp.com](https://testdoublephp.com/), а сам Double можна знайти на [GitHub](https://github.com/jasonmccreary/double).
