Mocking and Stubbing with Mockery
Isolate units with flexible test doubles.
Mocking and Stubbing with Mockery is a free PHP Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why a Dedicated Mocking Library
PHPUnit ships its own test-double API, but Mockery offers a more fluent, expressive syntax and capabilities PHPUnit's mocks lack — partial mocks, spies, flexible argument matching, and ordered expectations. For complex collaboration-heavy code, Mockery often reads far better. This lesson covers the full spectrum of doubles and when to use each.
composer require --dev mockery/mockeryStub vs Mock vs Spy
Precise vocabulary prevents confused tests:
- Stub — returns canned values; you assert on state (the result).
- Mock — has expectations on calls; you assert on behavior (it was called correctly). Failing expectations fail the test.
- Spy — records calls and lets you assert after the fact.
Rule of thumb: prefer stubs for queries, mocks/spies for commands.
A Stub with allows()
Use Mockery::mock() and allows() (or shouldReceive()->andReturn()) to provide canned return values without asserting the call happened. Here a gateway stub feeds a known exchange rate so we can test the calculation in isolation.
<?php
use Mockery;
use PHPUnit\Framework\TestCase;
final class ConverterTest extends TestCase
{
public function test_converts_using_rate(): void
{
$rates = Mockery::mock(RateGateway::class);
$rates->allows()->rateFor('USD', 'EUR')->andReturn(0.9);
$sut = new Converter($rates);
self::assertSame(90.0, $sut->convert(100.0, 'USD', 'EUR'));
Mockery::close();
}
}
A Mock with expects()
When the interaction itself is the contract — e.g. "an email must be sent exactly once" — use expects() or shouldReceive()->once(). Mockery verifies the expectation during Mockery::close(); an unmet call fails the test.
<?php
use Mockery;
$mailer = Mockery::mock(Mailer::class);
$mailer->expects()
->send(Mockery::type(Message::class))
->once();
$service = new SignupService($mailer);
$service->register('ada@example.com');
Mockery::close(); // fails here if send() was never called
Argument Matchers
Mockery's matchers let expectations be as loose or strict as needed:
Mockery::any()— any value.Mockery::type('string')/ a class name — type check.Mockery::on(fn($a) => ...)— custom predicate.Mockery::capture($var)— capture the argument for later assertions.
<?php
use Mockery;
$repo = Mockery::mock(UserRepo::class);
$repo->expects()
->save(Mockery::on(fn(User $u) => $u->isActive()))
->once()
->andReturnTrue();
Return Sequences and Dynamic Returns
You can script multiple return values across calls, or compute the return from arguments with andReturnUsing(). This models retry logic, pagination, or stateful collaborators.
<?php
use Mockery;
$api = Mockery::mock(HttpClient::class);
// First call throws, second succeeds (retry test):
$api->shouldReceive('get')
->twice()
->andThrow(new \RuntimeException('timeout'))
->andReturn('{"ok":true}');
// Or derive the return from the input:
$api->shouldReceive('echo')
->andReturnUsing(fn(string $in) => strtoupper($in));
Spies: Assert After the Fact
A spy flips the order: act first, then assert. Mockery::spy() records calls; afterward you query them with shouldHaveReceived(). Spies keep the Arrange-Act-Assert structure clean when you do not want pre-set expectations cluttering the setup.
<?php
use Mockery;
$logger = Mockery::spy(Logger::class);
$service = new PaymentService($logger);
$service->charge(500);
// Assertions happen AFTER the action:
$logger->shouldHaveReceived('info')
->with(Mockery::pattern('/charged 500/'))
->once();
Mockery::close();
Partial Mocks
Sometimes you want a real object but with one method overridden — a partial mock. Mockery's makePartial() calls through to real methods except those you set expectations on. Use sparingly: heavy reliance on partials usually signals a class doing too much.
<?php
use Mockery;
$report = Mockery::mock(Report::class)->makePartial();
// Only stub the slow/external method; the rest runs for real
$report->shouldReceive('fetchRawData')->andReturn(['a', 'b', 'c']);
// real summarize() runs, using the stubbed data
$summary = $report->summarize();
Don't Mock What You Don't Own
A core testing principle: avoid mocking third-party classes directly. Their APIs can change and your mock silently drifts from reality. Instead wrap them behind your own interface and mock that. The mock then verifies your contract, and an adapter integration test verifies the real binding.
<?php
interface PaymentGateway { // you own this
public function charge(int $cents, string $token): string;
}
final class StripeGateway implements PaymentGateway {
public function __construct(private \Stripe\StripeClient $client) {}
public function charge(int $cents, string $token): string {
return $this->client->paymentIntents->create([/* ... */])->id;
}
}
// Tests mock PaymentGateway, never \Stripe\StripeClient directly.
Always Close, and Verify Counts
Two operational must-dos:
- Call
Mockery::close()intearDown()(or useMockeryPHPUnitIntegrationtrait) so expectations are actually verified and globals cleaned. - Use explicit counts (
once(),times(n),never()) — vague mocks let bugs through.
<?php
use PHPUnit\Framework\TestCase;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
final class OrderServiceTest extends TestCase
{
use MockeryPHPUnitIntegration; // auto-calls Mockery::close()
public function test_does_not_refund_paid_orders(): void
{
$gw = \Mockery::mock(PaymentGateway::class);
$gw->shouldReceive('refund')->never();
// ... exercise SUT ...
}
}
Ordered Expectations
Occasionally call order is part of the contract — you must beginTransaction() before commit(). Mockery's ordered() enforces sequence, failing the test if calls arrive out of order. Use it only when order genuinely matters; over-specifying order makes brittle tests.
<?php
use Mockery;
$tx = Mockery::mock(Transaction::class);
$tx->shouldReceive('begin')->once()->ordered();
$tx->shouldReceive('commit')->once()->ordered();
$service = new TransferService($tx);
$service->run();
Mockery::close(); // fails if commit() happened before begin()
Quick Check
Stub or mock — which for what?
Recap
You mastered Mockery's doubles:
- Stubs (
allows) for queries; mocks (expects) for commands; spies for after-the-fact assertions. - Argument matchers (
type,on,capture) tune strictness. - Return sequences and
andReturnUsingmodel stateful/dynamic collaborators. - Partial mocks override single methods; use sparingly.
- Don't mock what you don't own — wrap third parties behind your interface.
- Always
Mockery::close()and assert explicit call counts.
Next: integration and functional testing.
Frequently asked questions
Is the “Mocking and Stubbing with Mockery” lesson free?
Yes — the full text of “Mocking and Stubbing with Mockery” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “Mocking and Stubbing with Mockery”?
Isolate units with flexible test doubles. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Mocking and Stubbing with Mockery” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The Test-Driven Development Workflow
- Mocking and Stubbing with Mockery
- Integration and Functional Testing
- Mutation Testing with Infection