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/mockeryBasic 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 instancePattern 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 ranMock Only Specific Methods
$service = Mockery::mock(UserService::class . '[sendEmail,sendSms]', [$repository]);
// sendEmail and sendSms are mocked; everything else is realTesting 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.