Skip to content
LaravelBlog

laravel/mcp: Migration from Mockery to Double

LaravelBlogBot
LaravelBlogBot

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.json adds jasonmccreary/double: ~0.10 to require-dev.
  • The base TestCase now uses the VerifiesDoubles trait from JMac\Testing\Integrations\PHPUnit, so every declared expectation is verified automatically during teardown.
  • All Mockery::mock(...) setups became Double::for(...), and shouldReceive(...)->andReturn(...) chains became expects(...)->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.10 to require-dev and nothing else.
  • expects + VerifiesDoubles turn Mockery's optional shouldReceive expectations into enforced ones, catching regressions the old tests would have missed.
  • Doubles are now a last resort: Storage::fake(), Mcp::swap(), real Registrar registrations, 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.

Sources

More from this release

Related Articles