Double: Mock PHP Classes in Tests With One API | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. [Composer Pacakge](https://www.msaied.com/public/articles?category=composer-pacakge)
6. /
7. Mock PHP Classes in Tests With the Double Library

   [Composer Pacakge](https://www.msaied.com/public/articles?category=composer-pacakge) [PHP](https://www.msaied.com/public/articles?category=php) 

 Mock PHP Classes in Tests With the Double Library
==================================================

 Double is a PHP 8.3 test double library by Jason McCreary that replaces mocks, spies, and partials with a single unified API — one constructor, three modes, and two setup verbs.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 11 Aug 2026 · Updated 11 Aug 2026 · 4 min read

ShareCopy linkCopied

 ![Mock PHP Classes in Tests With the Double Library](https://cdn.msaied.com/537/be45e68dffd72aa9bd833c2f409fcb07.png) 

  On this page +1. [What Is the Double Library?](#what-is-the-double-library)
2. [Creating a Double](#creating-a-double)
3. [Three Modes](#three-modes)
4. [Expectations and Argument Matching](#expectations-and-argument-matching)
5. [Verification](#verification)
6. [Failure Messages](#failure-messages)
7. [Migrating From Mockery](#migrating-from-mockery)
8. [Key Takeaways](#key-takeaways)

 What Is the Double Library?
---------------------------

[Double](https://github.com/jasonmccreary/double) is a PHP 8.3 test double library by Jason McCreary, the creator of Laravel Shift. Instead of choosing between a mock, a spy, and a partial at creation time, you create one kind of object — a *double* — and the verbs you use afterward determine how it behaves. The package is at v0.4.0 and works with PHPUnit 11 and 12 as well as Pest.

Install it as a dev dependency:

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

```

No service provider or configuration file is needed.

Creating a Double
-----------------

`Double::for()` accepts a class name, an interface, multiple interfaces, or a real instance:

```php
use JMac\Testing\Double;

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

```

The returned object satisfies `instanceof` and any type hint expecting the target, so it drops straight into a constructor without casting.

Three Modes
-----------

| Mode | Behaviour on unconfigured calls | |---|---| | **Loose** (default) | Returns a type-safe value (`false`, `0`, `[]`, a fresh double, etc.) | | **Strict** | Throws immediately | | **Passthru** | Delegates to a real instance and still records every call |

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

```

Expectations and Argument Matching
----------------------------------

Two verbs cover all setup: `expects()` (must be called) and `allows()` (may be called).

```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));

```

Call counts use a single `times()` method with named arguments instead of Mockery's chained helpers:

```php
$repository->expects('save');                    // exactly once
$repository->expects('save')->times(2);          // exactly twice
$repository->expects('save')->times(1, 3);       // between 1 and 3
$repository->expects('save')->times(minimum: 2); // at least 2
$repository->allows('save')->times(maximum: 5);  // at most 5
$repository->allows('save')->never();            // zero calls

```

The `Argument` facade handles looser matching:

```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('saveAll')->with(Argument::contains($book))->returns(true);

```

`Argument::capture($var)` writes the real argument into a variable for further assertions. `Argument::not()` negates any matcher without nesting.

Verification
------------

Call `verify()` at the end of a test, or add the `VerifiesDoubles` trait to your base test case and let it run automatically:

```php
use JMac\Testing\Integrations\PHPUnit\VerifiesDoubles;

class TestCase extends \PHPUnit\Framework\TestCase
{
    use VerifiesDoubles;
}

```

`received()` checks after the fact on any double — no spy declaration required:

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

```

`unused()` asserts a double received zero calls to any method and lists every call it actually saw.

Failure Messages
----------------

Double names the class you doubled (not a generated identifier), and on an unmet expectation it lists what the method was actually called with:

```javascript
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')`

```

A typo in a method name is caught at configuration time, not at the end of the test, and a "did you mean" suggestion is included when something close exists.

Migrating From Mockery
----------------------

The docs include a full method-by-method mapping. The most common conversions:

- `Mockery::mock(Foo::class)` → `Double::for(Foo::class)`
- `shouldReceive('foo')->once()->andReturn($x)` → `expects('foo')->returns($x)`
- `shouldHaveReceived('foo')` → `received('foo')`
- `Mockery::close()` → `verify()` or the `VerifiesDoubles` trait

A free [Double Converter](https://laravelshift.com/mockery-test-double-converter) from Laravel Shift automates the migration.

Key Takeaways
-------------

- One constructor (`Double::for()`) replaces mock, spy, and partial decisions.
- `expects()` and `allows()` are the only setup verbs; `times()` with named arguments handles all count shapes.
- Loose mode returns type-safe defaults so tests don't break on unconfigured calls.
- `received()` provides spy-style post-hoc assertions on every double.
- PHPUnit integration turns failures into proper assertion failures and the `VerifiesDoubles` trait removes manual `verify()` calls.
- Requires PHP 8.3; works inside or outside Laravel with PHPUnit 11/12 or Pest.

Full documentation is at [testdoublephp.com](https://testdoublephp.com/).

---

*Source: [Mock PHP Classes in Tests With the Double Library — Laravel News](https://laravel-news.com/double-php-testing-library)*

- [testing](https://www.msaied.com/public/articles?search=testing)
- [mocking](https://www.msaied.com/public/articles?search=mocking)
- [phpunit](https://www.msaied.com/public/articles?search=phpunit)
- [php](https://www.msaied.com/public/articles?search=php)
- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [packages](https://www.msaied.com/public/articles?search=packages)

 Frequently asked questions 
---------------------------

  How does Double differ from Mockery in PHP tests?Double uses a single `Double::for()` constructor instead of separate mock/spy/partial factories. Setup uses only two verbs — `expects()` and `allows()` — and spy-style `received()` checks are available on every double without declaring a spy upfront. There is no `Mockery::close()` equivalent; you call `verify()` or use the `VerifiesDoubles` trait instead.

   What PHP version does the Double library require?Double requires PHP 8.3 or higher. It works with PHPUnit 11 and 12 as well as Pest, inside or outside a Laravel application.

   Can I migrate an existing Mockery test suite to Double automatically?Yes. The Double documentation includes a full method-by-method mapping from Mockery to Double, and Laravel Shift provides a free Double Converter tool that automates the conversion.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleCursor Pagination and Lazy Collections at Scale in Laravel](https://www.msaied.com/public/articles/cursor-pagination-and-lazy-collections-at-scale-in-laravel) [Next articleNightOwl: Laravel Monitoring With Flat Pricing and Your Own PostgreSQL Storage](https://www.msaied.com/public/articles/nightowl-laravel-monitoring-with-flat-pricing-and-your-own-postgresql-storage)  

   On this page
-------------

1. [What Is the Double Library?](#what-is-the-double-library)
2. [Creating a Double](#creating-a-double)
3. [Three Modes](#three-modes)
4. [Expectations and Argument Matching](#expectations-and-argument-matching)
5. [Verification](#verification)
6. [Failure Messages](#failure-messages)
7. [Migrating From Mockery](#migrating-from-mockery)
8. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
