laravel/mcp: Migration from Mockery to Double
Contributed by jasonmccreary
Part of the mcp v1.0.2 release
Pull request #350 replaces Mockery with Double across the laravel/mcp test suite. The conversion was automated by Shift and then reviewed by hand, and it goes further than a mechanical swap: the suite now leans on real objects, Laravel facade fakes, and fixtures wherever possible, and it fixes the common misuse of Mockery's shouldReceive that let some tests pass without their expectations ever being exercised. Everything happens under tests/ and require-dev — src/ is untouched — so upgrading from v1.0.1 has no runtime impact, but the change is worth understanding if you contribute to or extend this package.
What changed
Three things landed in this PR:
composer.jsonaddsjasonmccreary/double: ~0.10torequire-dev.- The base
TestCasenow uses theVerifiesDoublestrait fromJMac\Testing\Integrations\PHPUnit, so every declared expectation is verified automatically during teardown. - All
Mockery::mock(...)setups becameDouble::for(...), andshouldReceive(...)->andReturn(...)chains becameexpects(...)->returns(...):
// Before (Mockery)
$this->registrar = Mockery::mock(Registrar::class);
$this->registrar
->shouldReceive('getLocalServer')
->with('demo')
->andReturn($server);
// After (Double)
$this->registrar = Double::for(Registrar::class);
$this->registrar
->expects('getLocalServer')
->with('demo')
->returns($server);
Several tests in McpInspectorCommandTest were deleted outright — for example handles http transport with https url and verifies process timeout is set correctly. These mostly stubbed a method, got the stub back, and asserted on the stub. They verified the mock, not the command.
Why expects instead of shouldReceive
Mockery's shouldReceive is lenient by design: unless you chain ->once(), the method is allowed to be called zero or more times. A test can declare a return value for a method that the code never actually invokes, and still pass. Double's expects is the opposite — the call must happen, and VerifiesDoubles fails the test if it doesn't. That's the "common misuse" this PR reworks: expectations that were silently optional become enforced contracts, and failures come with actionable messages rather than Mockery's sometimes-cryptic count mismatches.
Argument matching also gets more expressive. The route-parameter prompt test now verifies the actual Question object instead of trusting a loose match:
$output = Double::for(OutputStyle::class);
$output->expects('askQuestion')
->with(Argument::satisfies(
fn (Question $question): bool
=> $question->getQuestion() === 'What is the value for the [organisation] route parameter?'
))
->returns('4f8a1c2e');
$components = new Factory($output);
Fewer doubles, more real objects
The most valuable part of the PR is where doubles were removed entirely. In ResponseFromStorageTest, the old tests mocked FilesystemAdapter, threw a synthetic UnableToReadFile, and facade-mocked Storage::disk(). The new versions use the real disk abstraction:
// Before: three lines of Mockery wiring
Storage::shouldReceive('disk')->with(null)->andReturn($storage);
// After: a fake disk configured to throw on missing files
Storage::fake(config: ['throw' => true]);
Similarly, the MIME-type failure test now writes a real file with Storage::put('data/unknown', 'some-data') instead of stubbing mimeType to return false. In McpInspectorCommandTest, the beforeEach swaps in a genuine Registrar via Mcp::swap(new Registrar), and individual tests register real servers with Mcp::local('demo', ExampleServer::class) or Mcp::web('mcp/{organisation:uuid}', ExampleServer::class) using the existing fixture. The command then runs against real registration state, and doubles are reserved for the one boundary that genuinely can't be asserted — the console output style.
What this means for your upgrade
If you only consume laravel/mcp, the impact is a composer update: the only new package is a dev dependency, and there are no API, config, or behavior changes to the library itself. If you maintain a fork or have copied test patterns from this repo into your own MCP test suites, the conversion is straightforward and — as this PR demonstrated — automatable with Shift's Mockery-to-Test-Double converter. The mapping you'll use most often is Double::for(Class::class) for instantiation, expects('method')->with(...)->returns(...) for required calls, and the VerifiesDoubles trait on your base test case so expectations are actually checked.
Takeaways
- Test-suite-only change: upgrading from v1.0.1 adds
jasonmccreary/double: ~0.10torequire-devand nothing else. expects+VerifiesDoublesturn Mockery's optionalshouldReceiveexpectations into enforced ones, catching regressions the old tests would have missed.- Doubles are now a last resort:
Storage::fake(),Mcp::swap(), realRegistrarregistrations, and fixtures cover most scenarios. - Deleted tests that only asserted mock-to-mock behavior; the remaining tests exercise the actual commands.
- Contributors with Mockery-based tests can automate the migration with the Shift converter.