Skip to content

Mocking & Spying

Mocking and spying require Mockery, which is included with Laravel's default test dependencies.

Mocking

Replace execution entirely:

php
it("sends welcome email on registration", function () {
    SendWelcomeEmail::mock()->shouldExecute()->once()->with($user);

    $service->registerUser($user);

    // Mockery verifies the expectation automatically
});

it("returns a custom value from the mock", function () {
    CalculatePrice::mock()->shouldExecute()->andReturn(42.5);

    $result = $service->getPrice($product);

    expect($result)->toBe(42.5);
});

it("never sends email for inactive users", function () {
    SendWelcomeEmail::mock()->shouldNeverExecute();

    $service->registerInactiveUser($user);
});

The mock registers in the container, so any code that resolves the executable through ::sync(), ::onQueue(), or dependency injection gets the mock. Direct instantiation with new bypasses the container and won't use the mock.

Spying

Real execution + assertions after:

php
it("processes the order", function () {
    ProcessOrder::spy();

    $service->handleOrder($order);

    ProcessOrder::assert()->executed()->once()->with($order);
});

it("processes multiple orders", function () {
    ProcessOrder::spy();

    $service->handleOrders([$orderA, $orderB]);

    ProcessOrder::assert()->executed()->twice();
});

it("never processes cancelled orders", function () {
    ProcessOrder::spy();

    $service->handleCancelledOrder($order);

    ProcessOrder::assert()->notExecuted();
});

it("never processes with specific arguments", function () {
    ProcessOrder::spy();

    $service->handleOrders([$orderA, $orderB]);

    // Assert it was never called with this specific order
    ProcessOrder::assert()->notExecuted()->with($cancelledOrder);

    // Or use a callback for more complex matching
    ProcessOrder::assert()->notExecuted()->withArgs(fn($order) => $order->status === "cancelled");
});

Spies let the real execute() run. Use them when you want to verify the call happened without replacing behavior.

The executed() chain proxies to Mockery's verification API. Any Mockery verification method works (once(), twice(), times(), with(), withArgs(), ordered(), etc.).