Double: Mock PHP Classes in Tests With One API | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Mock PHP Classes in Tests With the Double Library        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)

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

 [  Composer Pacakge ](https://www.msaied.com/articles?category=composer-pacakge) [  PHP ](https://www.msaied.com/articles?category=php)  #testing   #mocking   #phpunit   #php   #laravel   #packages  

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

     11 Aug 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   What Is the Double Library?  ](#what-is-the-double-library)
2. [  02   Creating a Double  ](#creating-a-double)
3. [  03   Three Modes  ](#three-modes)
4. [  04   Expectations and Argument Matching  ](#expectations-and-argument-matching)
5. [  05   Verification  ](#verification)
6. [  06   Failure Messages  ](#failure-messages)
7. [  07   Migrating From Mockery  ](#migrating-from-mockery)
8. [  08   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)*

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fmock-php-classes-in-tests-with-the-double-library&text=Mock+PHP+Classes+in+Tests+With+the+Double+Library) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fmock-php-classes-in-tests-with-the-double-library) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  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. 

      Q02  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. 

      Q03  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. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Laravel AI SDK v0.10: 4 New Features Explained](https://cdn.msaied.com/540/e74363f048913d3782bc16a7292215db.png) Laravel AI SDK AI Agents Filesystem Tools 

### Laravel AI SDK v0.10: 4 New Features Explained

Laravel AI SDK v0.10 ships with filesystem tools for agents, human tool approval, and more. This walkthrough c...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 12 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-ai-sdk-v010-4-new-features-explained) [ ![NightOwl: Laravel Monitoring With Flat Pricing and Your Own PostgreSQL Storage](https://cdn.msaied.com/538/60582948c0339b19e872f4f3c03171f2.png) Laravel Monitoring PostgreSQL 

### NightOwl: Laravel Monitoring With Flat Pricing and Your Own PostgreSQL Storage

NightOwl redirects Laravel Nightwatch telemetry into a PostgreSQL database you own, replacing per-event billin...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 11 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/nightowl-laravel-monitoring-with-flat-pricing-and-your-own-postgresql-storage) [ ![Cursor Pagination and Lazy Collections at Scale in Laravel](https://cdn.msaied.com/536/3aab48ef4a4eaa26a3267637dc2ec8c7.png) laravel eloquent performance 

### Cursor Pagination and Lazy Collections at Scale in Laravel

Offset pagination breaks under large datasets. Learn how Laravel's cursor pagination and lazy collections let...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 11 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/cursor-pagination-and-lazy-collections-at-scale-in-laravel) 

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

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
