jasonmccreary/double
Double is a Laravel package by Jason McCreary for creating test doubles. It lets you swap real services for lightweight, configurable fakes that record method calls and return stubbed responses, keeping your test suite fast and free of heavy mocking boilerplate.
Install it as a dev dependency and keep it out of production autoloads:
composer require --dev jasonmccreary/double
Where to look first. The README is short by design, so read it once and then open src/. The whole library is a handful of classes and it is worth 20 minutes — every "why did my stub return null" question later is answered by knowing how the double is generated and how calls are recorded. The single entry point is the Double class; everything else hangs off Double::for($type).
First use case. Replace one slow or awkward collaborator in a single unit test. Don't start by doubling your whole dependency graph — start with the one class that makes the test need a database, network, or filesystem:
use Double\Double;
$mailer = Double::for(Mailer::class);
$mailer->stub('send')->returns(true);
$service = new WelcomeService($mailer);
$service->welcome('[email protected]');
$mailer->assertCalled('send');
That's the whole loop: create → stub → inject → act → assert on the double. If your very first test needs more than that, you're probably doubling something you should just instantiate for real.
The exact method names on the fluent API are the one thing worth confirming against the README before you commit to them — the surface is deliberately tiny and the package is young enough that signatures may still shift between minor versions.
Stub the collaborator, assert on the outcome. Reserve assertCalled for interactions that are genuinely part of the contract (an email must go out, a payment must be captured). For everything else, assert on the return value of the system under test — it keeps tests refactor-friendly.
Callback stubs for computed or argument-dependent returns. Literal return values get you 80% of the way; closures handle the rest:
$repo = Double::for(UserRepository::class);
$repo->stub('find')->returnsUsing(fn (int $id) => new User(['id' => $id]));
Use this instead of building elaborate fixture chains — a closure that mirrors the real contract is easier to read than three nested mock setups.
Exercise error paths cheaply. The thing you can never reproduce reliably against a real dependency:
$client = Double::for(StripeClient::class);
$client->stub('charge')->throws(new ApiConnectionException('timeout'));
$this->expectException(PaymentFailed::class);
(new Checkout($client))->process($order);
This is where doubles pay for themselves fastest — write these tests first when you're hardening a flaky integration.
Type-hint interfaces, double interfaces. If you type-hint a concrete class you inherit every problem with that class (final methods, constructor side effects, static calls). Doubling a small interface is the single highest-leverage habit with any double library, and it's also the cheapest way to keep Double working for you.
Laravel: swap at the container boundary, not at the call site.
$this->app->instance(PaymentGateway::class, $gateway);
Bind the double in setUp/beforeEach, then let the framework's own resolution do the injection. For facades, Double is the wrong tool — use Facade::swap() or shouldReceive, since the facade root is a static resolve that a generated double won't intercept.
Give yourself a one-line helper. In tests/Pest.php or a base TestCase:
function double(string $type): Double
{
return Double::for($type);
}
This is your seam. If you ever need to move back to Mockery or forward to a newer API, you change one function instead of two hundred tests.
Sequence and state. When behaviour depends on prior calls (retry logic, cursor pagination), capture state in the stub closure rather than reaching for a sequencing API:
$attempts = 0;
$client->stub('send')->returnsUsing(function () use (&$attempts) {
return ++$attempts < 3 ? false : true;
});
Explicit, greppable, and it survives API churn.
Never share a double between tests. Create it inside the test (or beforeEach), assert inside the test, let it die. Doubles are stateful recorders; a double held in a property across tests produces ordering-dependent failures that are miserable to debug.
Final classes and final methods cannot be doubled. Neither can enums, nor static methods. The fix is never "unfinal the class" — extract a narrow interface and depend on that. If you hit this often, treat it as design feedback rather than tooling friction.
The constructor is bypassed. The double satisfies the type but has none of the real object's initialised state. Any method you haven't stubbed will return a default (usually null or the type's zero value) or throw, depending on strictness — don't assume the parent's __construct ran. If you need configured state, stub the accessor.
Readonly properties and passthru. Passing a real object through to the underlying implementation while the double sits in front is the interaction most likely to surprise you. v1.0.1 fixes a failure where passthru blew up on inherited readonly properties — so if you double a class that inherits readonly state and rely on the real implementation being invoked, be explicitly on ^1.0.1 or later. If passthru against such a class still errors, the fallback is to stub the readonly accessor on the double rather than reading through to the parent.
Return types are enforced. Stubbing 'foo' on a method declared : int will blow up with a TypeError at call time, not at stub time. Read the real signature before you write the stub; the failure message points at the double's generated class, not your test, which makes it look scarier than it is.
self / static returns must be satisfied by returning the double itself, not a new instance. Fluent interfaces (builders, query objects) are the usual culprits.
Unexpected calls are your friend until they aren't. Strict doubles turn a method the SUT calls but you didn't anticipate into a hard failure. That's the good behaviour — it's how you find out your assumption about the collaborator was wrong. But it also means adding a method to an interface can break tests that "had nothing to do with it." Read those failures as "my stub is incomplete," not "the library is annoying."
Argument matching is where stubs silently don't apply. If a stub seems ignored, it's almost always because the SUT passed different arguments than you specified. Log the recorded calls ($double->calls() or equivalent) before you start rewriting the stub — nine times out of ten the SUT is calling with '1' and you stubbed 1.
get_class() equality breaks; instanceof doesn't. The double is a generated subclass or anonymous implementation. Any assertion or container lookup keyed on exact class name will fail.
void and never methods can't be given a return value — stub them for side effects or throws only, and don't chain a returns() onto them.
Assertions are one-shot per test. Call assertCalled after the act, never in setUp, and never twice expecting cumulative counts unless the API explicitly says so.
Version pinning matters more than usual here. Single maintainer, small install base, a fast-moving API — and patch releases carry real behavioural fixes (v1.0.1 changed passthru semantics for readonly properties, which is exactly the kind of thing that could silently flip a test green or red). Lock the version in composer.lock, run the suite in CI on the pinned version, and read the changelog before any bump. A one-line "Fixed" entry can be a one-character change with a large blast radius.
Keep it at the boundary. Don't double value objects, DTOs, collections, or anything you can construct with two lines of real code. The cost of a double is cognitive, not runtime — every one you add is a place where the test asserts your assumption about a collaborator rather than its actual behaviour.
How can I help you explore Laravel packages today?