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/01M22N44A70A5MC2S599JP0MPH.webp)  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) 

 [ ![Octane State Leakage: Detecting and Fixing Shared-Memory Bugs in Laravel Workers](https://cdn.msaied.com/705/8e7dc5a87f9f9a30b8523ca5280e8f97.png) laravel octane performance 

### Octane State Leakage: Detecting and Fixing Shared-Memory Bugs in Laravel Workers

Laravel Octane keeps workers alive across requests, making shared state a silent killer. Learn how to detect,...

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

 26 Sep 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/octane-state-leakage-detecting-and-fixing-shared-memory-bugs-in-laravel-workers) [ ![Production AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts](https://cdn.msaied.com/704/165bcb76b898fe9911130d2c93b9b805.png) laravel ai llm 

### Production AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts

Building reliable AI agents in Laravel means more than wiring up an API call. Learn how to stream responses sa...

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

 26 Sep 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/production-ai-agents-in-laravel-streaming-token-budgets-and-structured-output-contracts-4) [ ![Decide with Jev: Build a Laravel AI Content Preflight Checker That Returns a Probability](https://cdn.msaied.com/701/916a0dd2b427c6335395d6d2684524ab.png) Laravel AI Jev TypeSafe 

### Decide with Jev: Build a Laravel AI Content Preflight Checker That Returns a Probability

Jev is a TypeSafe AI model that returns a probability score instead of text. Learn how Harris Raftopoulos uses...

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

 25 Sep 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/decide-with-jev-build-a-laravel-ai-content-preflight-checker-that-returns-a-probability) 

   [  ![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)
