Mockery PHP: Complete Mocking Guide for PHP Testing

Mockery PHP: Complete Mocking Guide for PHP Testing

Mockery is PHP's most powerful mocking library, popular in Laravel and Symfony ecosystems. It provides a fluent API for creating test doubles: mocks, stubs, spies, and partial mocks. This guide covers creating mocks, defining expectations with shouldReceive, argument matchers, mock verification, spies, and using Mockery with PHPUnit and Pest.

Key Takeaways

Mockery::mock(Interface::class) creates a mock. ->shouldReceive('method') defines what to expect. The fluent chain reads like English: "this mock should receive 'sendEmail' once with 'alice@example.com'".

Always call Mockery::close() in tearDown. This verifies all expectations defined with shouldReceive. Without it, unmet expectations pass silently.

->andReturn($value) stubs a return value. ->once(), ->twice(), ->times(N) assert call count. Combine them: ->shouldReceive('save')->once()->andReturn(true).

Use Mockery::spy() when you want to verify calls after-the-fact. Spies don't set up expectations in advance — they record calls and let you assert them at the end of the test.

Mockery::mock(ConcreteClass::class) works but requires the class to have non-final public methods. Prefer mocking interfaces over concrete classes.

What Is Mockery?

Mockery is a PHP mocking library that complements PHPUnit and Pest. While PHPUnit includes a basic mock object system, Mockery provides a richer API with:

  • More expressive fluent syntax
  • Spy support
  • Partial mocks
  • Hamcrest-style argument matchers
  • Better error messages on expectation failures

Mockery is the default mocking library in Laravel and is widely used in the Symfony ecosystem.

Installing Mockery

composer require --dev mockery/mockery

Basic Mock Creation

use Mockery;
use PHPUnit\Framework\TestCase;

class PaymentServiceTest extends TestCase
{
    public function tearDown(): void
    {
        Mockery::close(); // REQUIRED — verifies all expectations
        parent::tearDown();
    }

    public function test_charge_calls_gateway_once(): void
    {
        // Create mock
        $mockGateway = Mockery::mock(PaymentGateway::class);

        // Define expectation
        $mockGateway->shouldReceive('charge')
                    ->once()
                    ->with('tok_visa', 9999)
                    ->andReturn(['status' => 'success', 'id' => 'ch_123']);

        // Inject and use
        $service = new PaymentService($mockGateway);
        $result = $service->charge('tok_visa', 99.99);

        $this->assertEquals('success', $result['status']);
    }
}

Mockery::close() in tearDown() is not optional — it's what verifies that ->once() expectations were actually called.

shouldReceive: Defining Expectations

// Called any number of times
$mock->shouldReceive('method');

// Called exactly once
$mock->shouldReceive('method')->once();

// Called exactly twice
$mock->shouldReceive('method')->twice();

// Called exactly N times
$mock->shouldReceive('method')->times(3);

// Called at least once
$mock->shouldReceive('method')->atLeast()->once();

// Called at most N times
$mock->shouldReceive('method')->atMost()->times(2);

// Called between N and M times
$mock->shouldReceive('method')->between(2, 5);

// Never called
$mock->shouldReceive('method')->never();
// or:
$mock->shouldNotReceive('method');

Return Values

// Return a specific value
$mock->shouldReceive('findById')->andReturn(new User(1, 'Alice'));

// Return null
$mock->shouldReceive('findByEmail')->andReturnNull();

// Return different values on consecutive calls
$mock->shouldReceive('getStatus')
     ->andReturn('pending', 'processing', 'complete');

// Throw an exception
$mock->shouldReceive('save')->andThrow(new DatabaseException('Connection lost'));

// Execute a closure
$mock->shouldReceive('process')->andReturnUsing(function ($input) {
    return strtoupper($input);
});

// Return self (for method chaining)
$mock->shouldReceive('where')->andReturnSelf();
$mock->shouldReceive('limit')->andReturnSelf();
$mock->shouldReceive('get')->andReturn(collect([$item]));

Argument Matching

Exact Values

$mock->shouldReceive('sendEmail')
     ->with('alice@example.com', 'Welcome!');

Any Value

$mock->shouldReceive('log')
     ->with(Mockery::any(), Mockery::any());

Type Checking

$mock->shouldReceive('process')
     ->with(Mockery::type('int'));

$mock->shouldReceive('save')
     ->with(Mockery::type(User::class)); // must be User instance

Pattern Matching

$mock->shouldReceive('log')
     ->with(Mockery::pattern('/^\[ERROR\]/'));

Closure-Based Matching

$mock->shouldReceive('charge')
     ->with(Mockery::on(function ($amount) {
         return $amount > 0 && $amount <= 10000;
     }));

Multiple Arguments with Mixed Matchers

$mock->shouldReceive('transfer')
     ->with(
         Mockery::type('int'),           // account ID
         Mockery::on(fn($a) => $a > 0), // positive amount
         Mockery::any()                  // currency
     );

Ignore Remaining Arguments

$mock->shouldReceive('query')
     ->withArgs(function (string $sql, ...$bindings) {
         return str_contains($sql, 'SELECT * FROM users');
     });

Chaining Expectations

$mock->shouldReceive('process')
     ->once()
     ->with(Mockery::type(Order::class))
     ->andReturn(['success' => true]);

// Multiple different calls
$mock->shouldReceive('getUser')->with(1)->once()->andReturn($alice);
$mock->shouldReceive('getUser')->with(2)->once()->andReturn($bob);
$mock->shouldReceive('getUser')->with(Mockery::any())->andReturnNull();

Spies: Record Then Assert

Spies don't define expectations upfront — they record all calls and let you assert after:

public function test_order_service_notifies_after_completion(): void
{
    $spy = Mockery::spy(NotificationService::class);

    $service = new OrderService($spy);
    $service->completeOrder(Order::factory()->make());

    // Assert after the fact
    $spy->shouldHaveReceived('notifyUser')->once();
    $spy->shouldHaveReceived('notifyAdmin')->with(Mockery::type(Order::class));
    $spy->shouldNotHaveReceived('sendRefund');
}

Spies are useful when you don't know in advance what methods will be called, or when you're testing a behavior-first (verify after acting) style.

Partial Mocks

Partial mocks let you mock some methods while keeping the real implementation for others:

$partial = Mockery::mock(UserService::class)->makePartial();
$partial->shouldReceive('sendEmail')->once()->andReturn(true);

// Calls to other methods use the real implementation
$user = $partial->createUser(['email' => 'alice@example.com', 'name' => 'Alice']);
$this->assertEquals('Alice', $user->name); // real createUser logic ran

Mock Only Specific Methods

$service = Mockery::mock(UserService::class . '[sendEmail,sendSms]', [$repository]);
// sendEmail and sendSms are mocked; everything else is real

Testing Protected/Private Methods

Use Mockery::mock with a class name and shouldAllowMockingProtectedMethods():

$mock = Mockery::mock(MyClass::class)
               ->makePartial()
               ->shouldAllowMockingProtectedMethods();

$mock->shouldReceive('protectedMethod')->once()->andReturn('test');

Warning: Mocking protected methods is usually a design smell. It often means the class needs refactoring.

Mockery with Pest

Mockery integrates naturally with Pest:

<?php

use App\Services\PaymentService;
use App\Contracts\PaymentGateway;
use Mockery;

beforeEach(function () {
    $this->mockGateway = Mockery::mock(PaymentGateway::class);
    $this->service = new PaymentService($this->mockGateway);
});

afterEach(fn() => Mockery::close());

test('charge calls gateway with correct amount', function () {
    $this->mockGateway
        ->shouldReceive('charge')
        ->once()
        ->with(Mockery::type('string'), 4999)
        ->andReturn(['status' => 'success']);

    $result = $this->service->charge('tok_test', 49.99);

    expect($result['status'])->toBe('success');
});

test('refund propagates gateway exception', function () {
    $this->mockGateway
        ->shouldReceive('refund')
        ->once()
        ->andThrow(new \RuntimeException('Gateway timeout'));

    expect(fn() => $this->service->refund('ch_123'))
        ->toThrow(\RuntimeException::class, 'Gateway timeout');
});

Mockery Alias Mocks

Alias mocks let you mock static methods and classes that can't be injected:

// Mock a static class
$mock = Mockery::mock('alias:' . Carbon::class);
$mock->shouldReceive('now')->andReturn(Carbon::parse('2026-01-01'));

// Mock final classes
$mock = Mockery::mock('overload:' . FinalClass::class);

Note: Alias mocks only work if the class hasn't been loaded yet. They're brittle — prefer dependency injection and avoid needing them.

Mockery vs PHPUnit Mock Objects

Feature Mockery PHPUnit Mock Objects
Syntax Fluent (->shouldReceive()) Builder-based (->method()->willReturn())
Spies Yes (Mockery::spy()) No
Partial mocks Yes (->makePartial()) Limited
Argument matchers Rich (type, pattern, closure) $this->callback(), $this->isType()
Consecutive returns ->andReturn(a, b, c) ->willReturnOnConsecutiveCalls(a, b, c)
Closure-return ->andReturnUsing(fn) ->willReturnCallback(fn)
Call verification ->once() inline ->expects($this->once()) before setup

Both work well. Mockery is more expressive; PHPUnit mocks are zero-dependency. For Laravel projects, Mockery is standard.

Full Example

class OrderProcessorTest extends TestCase
{
    private MockInterface $mockInventory;
    private MockInterface $mockPayment;
    private MockInterface $mockEmail;
    private OrderProcessor $processor;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mockInventory = Mockery::mock(InventoryService::class);
        $this->mockPayment   = Mockery::mock(PaymentGateway::class);
        $this->mockEmail     = Mockery::mock(EmailService::class);
        $this->processor = new OrderProcessor(
            $this->mockInventory,
            $this->mockPayment,
            $this->mockEmail
        );
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    public function test_process_order_success_flow(): void
    {
        $order = new Order(items: [new OrderItem(productId: 1, quantity: 2)], total: 49.99);

        $this->mockInventory
            ->shouldReceive('reserve')
            ->once()
            ->with($order->items)
            ->andReturn(true);

        $this->mockPayment
            ->shouldReceive('charge')
            ->once()
            ->with(Mockery::type('string'), 4999) // cents
            ->andReturn(['id' => 'ch_ok', 'status' => 'success']);

        $this->mockEmail
            ->shouldReceive('sendOrderConfirmation')
            ->once()
            ->with(Mockery::on(fn($o) => $o->id === $order->id));

        $result = $this->processor->process($order, 'tok_visa');

        $this->assertTrue($result->success);
        $this->assertEquals('ch_ok', $result->chargeId);
    }

    public function test_process_order_inventory_failure_does_not_charge(): void
    {
        $order = new Order(items: [new OrderItem(productId: 99, quantity: 100)], total: 999.99);

        $this->mockInventory
            ->shouldReceive('reserve')
            ->once()
            ->andReturn(false); // out of stock

        $this->mockPayment->shouldNotReceive('charge');
        $this->mockEmail->shouldNotReceive('sendOrderConfirmation');

        $result = $this->processor->process($order, 'tok_visa');

        $this->assertFalse($result->success);
        $this->assertEquals('out_of_stock', $result->failureReason);
    }
}

Common Mistakes

Forgetting Mockery::close(): Expectations are verified at close. Without it, ->once() never fails even if the method was never called.

Mocking what you don't own: Don't mock third-party classes you don't control. Wrap them in your own interface and mock the interface.

Over-specifying arguments: Using ->with(Mockery::any()) when you care about the value, or specifying exact complex objects when you only care about a field.

Partial mocks on final classes: Mockery can't partial-mock final classes without alias mocking. Make classes non-final or use interfaces.

End-to-End Testing with HelpMeTest

Mockery handles your PHP unit tests in isolation. For testing your deployed PHP application end-to-end — browser flows, JavaScript interactions, integration with third-party services — HelpMeTest runs Robot Framework + Playwright tests against your live environment.

HelpMeTest's 24/7 monitoring ensures you catch production regressions between deployments. Free tier: 10 tests. Pro: $100/month flat.

Read more