skip to content

In a Laravel test, how do Cache::shouldReceive(), a facade spy and $this->mock() differ when you replace a collaborator?

level: middleimportance: should knowfreq 48%

answer

  1. Mockery underneath all three
  2. expectations up front vs assertions after
  3. unexpected call on a full mock throws
  4. $this->mock() binds via instance()
  5. never mock Request or Config

basics

~10 s

Cache::shouldReceive() swaps the facade's instance for a Mockery mock with expectations set up front; a spy records every call for shouldHaveReceived() afterwards; $this->mock(Service::class) binds a Mockery mock in the container for injected classes.

solid answer

~40 s

All three are Mockery objects put where the app will look for them. `Cache::shouldReceive('get')->with('refund-limit')->andReturn(500)` replaces the object behind the `Cache` facade with a **full mock**, so any call you did not expect fails; `Cache::expects('get')` does the same and also expects exactly one call. A **spy**, `Cache::spy()` or `$this->spy(Service::class)`, accepts any call and lets you assert afterwards with `shouldHaveReceived('put')->with(...)`. For a class your code type-hints, `$this->mock(RefundGateway::class, fn (MockInterface $m) => $m->expects('refund'))` binds the mock with `$this->app->instance()`; `partialMock()` keeps unlisted methods real. Expectations are verified when the test tears down. Do not mock `Request` or `Config`: send input through the HTTP test methods and call `Config::set()`. For Mail, Queue or Http, prefer the dedicated fake.

code

php · 16 lines
php
<?php

use App\Services\RefundGateway;
use Mockery\MockInterface;

test('a failed gateway refund is reported to the user', function () {
    $this->mock(RefundGateway::class, function (MockInterface $mock) {
        $mock->expects('refund')->andThrow(new RuntimeException('declined'));
    });

    $order = App\Models\Order::factory()->paid()->create();

    $this->actingAs($order->customer)
        ->post("/orders/{$order->id}/refunds/instant", ['amount' => 1500])
        ->assertSessionHas('error');
});

go deeper

for a junior

Recall that Cache::shouldReceive() sets a mocked return, a spy is checked afterwards with shouldHaveReceived(), and $this->mock() replaces an injected class.

for a middle

Explain full versus partial mocks, what expects() adds over shouldReceive(), and that $this->mock() works through the container's instance binding.

for a senior

Choose dedicated fakes over facade mocks for Mail, Queue and Http, and spot tests that mock Request or Config or bind mocks too late to take effect.

for a principal

Set a rule for when a test may mock versus fake versus use the real thing, so suites stay readable and do not pin internal call sequences.

## Why these helpers exist Laravel's test case wraps **Mockery**, a PHP mocking library, so you can replace a collaborator with a test double without wiring it by hand. The three helpers differ in **where** the double is installed and **when** you state what should happen. ## Facade expectations: `shouldReceive()` and `expects()` Every facade class inherits `shouldReceive()` and `expects()`. The first call creates a Mockery mock of the class currently behind the facade, **swaps** it in (both on the facade and in the container), and returns a Mockery expectation: ```php Cache::shouldReceive('get') ->with('refund-limit') ->andReturn(500); $this->post("/orders/{$order->id}/refunds", ['amount' => 600]) ->assertSessionHasErrors('amount'); ``` Key behaviours: - It is a **full mock**: a call to any method you did not set up, say `Cache::put()`, throws instead of reaching a real cache. - `shouldReceive()` on its own allows the method to be called any number of times, including zero. Add `->once()` or `->times(2)` to make the count part of the test. - `expects()` is Mockery's shorthand for an expectation that must be met exactly once; the current Laravel docs use it in their examples. - `Facade::partialMock()` returns a partial mock, so methods without an expectation run for real. ## Spies: assert after the fact A **spy** accepts every call and records it, and you assert afterwards. That reads in arrange-act-assert order: ```php Cache::spy(); $this->get('/refunds/summary')->assertOk(); Cache::shouldHaveReceived('put')->with('refund-summary', Mockery::any(), 600); ``` The generic facade spy returns `null` from every call. The `Cache` facade overrides `spy()` to wrap the real cache as a partial spy when one is available, so cache calls keep working while being recorded. ## Container mocks: `$this->mock()`, `partialMock()`, `spy()` Classes that are **injected** rather than reached through a facade are replaced in the container. The base test case offers: | Helper | What it binds | Unlisted methods | |---|---|---| | `$this->mock(RefundGateway::class, fn ($m) => ...)` | a Mockery mock | throw | | `$this->partialMock(RefundGateway::class, fn ($m) => ...)` | a partial mock | run for real | | `$this->spy(RefundGateway::class)` | a Mockery spy | return `null`, recorded | | `$this->instance(RefundGateway::class, $object)` | any object you built | whatever that object does | The first three all go through `instance()`, which calls `$this->app->instance()`. The binding only reaches code that resolves the class **after** it is registered, so set mocks up before the request. ## Real-time facades A class imported with the `Facades\` prefix, for example `use Facades\App\Services\RefundGateway;`, is a **real-time facade**: Laravel generates a facade for it on the fly. It inherits the same helpers, so `RefundGateway::shouldReceive('refund')->once()` works exactly as it does for `Cache`. That gives static-looking call sites the same testability as constructor injection, which is one reason teams accept them. ## When verification happens During teardown the test case adds Mockery's expectation count to the test's assertions and calls `Mockery::close()`. That is where a `->once()` that was never met fails the test. You do not need to call `Mockery::close()` yourself in a Laravel test case. ## What not to mock 1. **`Request`.** Pass the input through `get()`, `post()` or `json()` instead; a mocked request bypasses routing, middleware and validation. 2. **`Config`.** Call `Config::set('refunds.limit', 500)` or `config([...])`; a mocked config repository breaks every other config read. 3. **Services with a fake.** `Mail`, `Queue`, `Bus`, `Event`, `Notification`, `Http`, `Storage` and `Process` have purpose-built fakes with readable assertions. `Mail::shouldReceive('send')` is brittle next to `Mail::fake()` and `Mail::assertSent()`. ## Choosing - Need a return value and a strict call contract up front: `shouldReceive`/`expects`. - Want to assert after the action and keep the test readable: a spy. - The collaborator is type-hinted in a constructor: `$this->mock()` or `partialMock()`.

  • A test uses Cache::shouldReceive('get') and the code under test also calls Cache::put(). What happens?
    The facade is now a full Mockery mock, so the unexpected `put()` call throws a Mockery exception and the test errors. Either add an expectation for `put`, use `Cache::partialMock()` so unlisted methods run for real, or switch to `Cache::spy()` and assert afterwards.
  • Why is $this->mock(RefundGateway::class) sometimes ignored by the code under test?
    It registers the mock with `$this->app->instance()`. Code that already resolved and kept a `RefundGateway`, for example a singleton built earlier in the test, still holds the real object. Register the mock before anything resolves the class, typically at the top of the test.

saying these in an interview costs you the question

  • Cache::shouldReceive() only intercepts the named method and lets other calls reach the real cache
  • shouldReceive() without once() fails if the method is never called
  • Mocking the Request facade is the normal way to feed input to a feature test
  • You must call Mockery::close() yourself in every Laravel test
  • A spy must declare its expectations before the code runs