Laravel's Macroable trait lets you add methods to classes you don't own, including classes from third-party packages. But it introduces a wrinkle into the way we normally mock those classes for testing.
Let's walk through an example to demonstrate the problem and solution.
Suppose a package ships a ReportClient class behind a Reports facade, and its generate() method is naively hardcoded to always use the app's current locale.
We wanted a way to generate a report in a specific locale, so we added a macro method that swaps the locale just for the duration of the call, and still calls the original underlying class method:
ReportClient::macro('generateInLocale', function (string $locale) {
$original = App::getLocale();
App::setLocale($locale);
try {
return Reports::generate();
} finally {
App::setLocale($original);
}
});
Normally when testing this, we would mock the facade using $this->mock() and chain on a shouldReceive() for our new method, then assert what it did.
But this normal approach fails.
The macro method we added only works through the __call() method Macroable adds to the real class.
Here's the problem though.
When you swap out the instance with a mock, all of those methods the Macroable trait added are now gone from the mocked class.
Aaron ran into this on one of our projects, and I really liked how he solved it:
public function testGeneratesReportInRequestedLocale(): void
{
// A partial mock keeps Macroable __call (a facade shouldReceive() mock would lose the macro).
// andReturnUsing() runs while generate() executes, the only moment the locale is swapped,
// so it's where we assert the correct locale was used.
$mock = Mockery::mock(ReportClient::class)->makePartial();
$mock->shouldReceive('generate')
->once()
->andReturnUsing(fn() => self::assertSame('fr', App::getLocale()));
Reports::swap($mock);
Reports::generateInLocale('fr');
}
First, the main solution was to use a partial mock.
This keeps every real method, including the __call() that makes macros work, while still letting us mock generate().
But there's something more interesting going on here too.
Take a look at the andReturnUsing method.
Normally, we would use this to control what value gets returned by our mocked function.
But instead, we're using it to test the state of the application in the brief moment while this generate() method is running.
This lets us prove that the locale is swapped as expected.
And best of all, notice the comment. The test works without it, but the next developer who touches it won't have to rediscover why a normal facade mock breaks everything.
Here to help,
Joel
P.S. Mocking trips up a lot of Laravel developers. We built a whole workshop to help you write tests with confidence, even in tricky situations like this one.