Testing stateful API endpoints in Laravel, you may come across this exception message:
BadMethodCallException: Method Mockery_1_Illuminate_Cache_CacheManager::store() does not exist on this mock object
In the official documentation, the facade service mock example covers Cache::shouldReceive() but there are potential limitations. Generally APIs are stateless, so developers are less likely to hit this roadblock. Here’s a solution to it anyhow!
Mocking the Cache Facade
The problem is from any integration test using the MakesHttpRequests
trait to send Illuminate\Support\Request
instances through Laravel’s app container lifecycle. For instance:
/** @test */
function it_cache_busts_metrics_when_sending_a_message()
{
$recipient = factory(User::class)->create();
$sender = factory(User::class)->create();
Cache::shouldReceive('forget')
->once()
->with("user:metrics:{$sender->id}")
->andReturn(true);
$this->actingAs($sender)
->json('POST', "/user/{$recipient->id}/message", [
'body' => 'Hello world!',
])
->assertSuccessful()
->assertJson([
'message' => 'Message sent!',
'data' => [
'sender_id' => $sender->id,
'recipient_id' => $recipient->id,
'body' => 'Hello world!',
],
]);
}
After the Cache
facade mock is setup, when making the POST HTTP call Laravel may hit framework class methods:
Illuminate\Routing\RoutingServiceProvider@registerRedirector()
- for session-flashed data supporting old() form re-hydration
Illuminate\Session\SessionManager@createCacheBased()
The latter method is what calls CacheManager@store()
as result of routing dependencies. When the test environment uses a cache driver like Redis or Memcache, this error will occur since the $app['cache']
has a mock that isn’t a partial so any method call not mocked will throw the above exception.
Most devs use the file
or array
cache drivers for local testing (as setup in phpunit.xml) and neither call createCacheBased()
. That makes this exception not noticeable until a continuous integration environment (CircleCI, Travis) is introduced, including a cache server instance.
Explicitly allow store()
on Cache
facade mock
When using Redis as a queue driver for tests, I worked around this by just giving the app container what it wants, making store()
a pass-through method for the concrete implementation. Then setup mock expectations for the test case that document what parts of the cache driver should be called.
protected function mockMetricsCacheBusting($user)
{
$store = Cache::store();
Cache::shouldReceive('store')->andReturn($store);
Cache::shouldReceive('forget')
->once()
->with("user:metrics:{$user->id}")
->andReturn(true);
return $this;
}
$this->mockMetricsCacheBusting($sender)
->actingAs($sender)
->json('POST', "/user/{$recipient->id}/message", [
// etc.