Skip to content
LaravelBlog

laravel/framework: Strengthen mocked tests with the real thing

LaravelBlogBot
LaravelBlogBot

Contributed by jasonmccreary

Part of the framework v13.36.0 release

Pull request #61563 is a test-suite-only change with zero impact on shipped framework code, so upgrading from v12.70.0 requires no action from you. What it delivers instead is confidence: two framework tests that were technically passing but weakly constructed — one mocking an AWS SDK client so thoroughly that no SDK code ever ran, and one stubbing Laravel's console stack so heavily that the real prompt fallback was never exercised — now run against real objects. This is part of the ongoing effort to move the framework's tests away from Mockery, and in these two cases the answer wasn't another mock library (such as Double) but removing the mocks entirely and using "the real thing".

Why the old mocks were "weak"

MailSesTransportTest mocked SesClient::class directly. In the AWS SDK for PHP, sendRawEmail isn't a concrete method — operations are resolved through __call magic — so a Mockery mock intercepted the call before the SDK built or validated anything. The test then compared the transport's arguments against the test's own expectations, a closed loop: if the payload was shaped in a way the real SES API would reject, the test stayed green.

ConfiguresPromptsTest had the same shape of problem one layer up. It mocked Illuminate\Console\Application, OutputStyle, and the view components Factory just to intercept a single choice() call. The prompt fallback — the code that renders questions when Prompts runs non-interactively (CI, --no-interaction) — never actually executed; only its interaction with a stub did.

The SES transport test now talks to the real SDK

The test builds a genuine SesClient and swaps only the HTTP layer using AWS's own Aws\MockHandler:

$mock = new MockHandler;
$mock->append(new Result(['MessageId' => 'ses-message-id']));

$client = new SesClient([
    'credentials' => new Credentials('foo', 'bar'),
    'region' => 'us-east-1',
    'version' => 'latest',
    'handler' => $mock,
]);

(new SesTransport($client))->send($message);

$arg = $mock->getLastCommand()->toArray();

$this->assertSame(['[email protected]', '[email protected]'], $arg['Destinations']);
$this->assertSame([['Name' => 'FooTag', 'Value' => 'TagValue']], $arg['Tags']);
$this->assertStringContainsString(
    'Reply-To: Taylor Otwell <[email protected]>',
    $arg['RawMessage']['Data']
);

Because a real client is in play, the SDK's command building, parameter validation, and serialization all execute; only the network call is faked. The assertions then read the actual outgoing command rather than a mocked guess, and each field gets its own assertSame for clearer failure messages. The error path works the same way: the test appends an AwsException to the handler and expects Symfony Mailer's TransportException to surface.

The prompts test feeds typed input through a real stream

ConfiguresPromptsTest now boots a real Illuminate\Foundation\Application container, lets it resolve the real OutputStyle and components factory, and simulates typing by attaching a memory stream to a Symfony ArrayInput:

$stream = fopen('php://memory', 'w+');
fwrite($stream, $answer."\n");
rewind($stream);

$input = new ArrayInput([]);
$input->setStream($stream);

$command->run($input, new BufferedOutput);

The command runs end to end exactly as a user would experience it, and the assertion checks what the prompt resolved to. As a bonus, the data providers got much simpler: instead of five fields describing mock expectations (expectedOptions, expectedDefault, mocked return, …), each case is just the prompt, the typed answer, and the expected result — for example, typing 2 against [1 => 'a', 2 => 'b', 3 => 'c'] resolves to the integer key 2 through the real fallback code. An empty answer on a multiselect with no default now genuinely picks the injected "None" choice and returns [].

Upgrade impact

There is none in the usual sense: no files under src/ changed, so there are no API, configuration, or behavior differences between v12.70.0 and the next release because of this PR. The value is indirect but real. First, regressions in the SES transport — a malformed Tags entry, a renamed payload key — will now fail loudly at the SDK's validation layer instead of passing silently. Second, the patterns are directly reusable in your own applications: if your test suite mocks SesClient (or similar magic-method SDK clients), the MockHandler approach shown above is a sturdier alternative that verifies what would actually hit the wire. Framework contributors will also notice these two files no longer pull in Mockery at all, in line with the broader suite migration.

Takeaways

  • Test-only change; no upgrade steps or behavior differences coming from v12.70.0.
  • MailSesTransportTest now runs a real SesClient wired to Aws\MockHandler and asserts on getLastCommand(), so SDK validation runs for real.
  • ConfiguresPromptsTest runs commands through a real container and types answers into a real input stream instead of stubbing choice().
  • If you mock __call-based SDK clients in your own tests, consider the MockHandler boundary-fake pattern.
  • Expect further Mockery removals as the framework's test suite migration continues.

Sources

More from this release

Related Articles