Mocking Best Practices (PHPUnit & Mockery)
Also Known As
mock best practices
test mocking
PHPUnit mocks
TL;DR
Guidelines for effective mocking: mock interfaces not classes, avoid over-mocking, prefer stubs for queries and mocks for commands.
Explanation
Key mocking guidelines: mock interfaces rather than concrete classes (easier to substitute, forces DIP compliance). Distinguish stubs (return canned data — no assertions on calls) from mocks (assert that specific calls were made — for commands with side effects). Avoid over-mocking: if a test mocks 5 collaborators, the unit under test has too many dependencies — refactor. Avoid mocking types you don't own (3rd-party SDKs); wrap them in an adapter and mock the adapter. Use Mockery's shouldReceive for expressive expectations or PHPUnit's createMock. Test the interface contract, not the implementation — if an internal refactor breaks tests without changing behaviour, the tests were too tightly coupled to implementation.
Diagram
flowchart TD
subgraph Test_Double_Types
DUMMY[Dummy - passed but not used]
STUB[Stub - returns canned values]
SPY[Spy - records calls for assertions]
MOCK2[Mock - pre-programmed expectations]
FAKE[Fake - working implementation<br/>InMemoryRepository]
end
subgraph When_to_Mock
MOCK_YES[External APIs<br/>Email and payment services<br/>Database in unit tests<br/>Time and random]
MOCK_NO[Internal pure functions<br/>Value objects<br/>Domain logic]
end
style FAKE fill:#238636,color:#fff
style MOCK2 fill:#1f6feb,color:#fff
style MOCK_YES fill:#238636,color:#fff
style MOCK_NO fill:#d29922,color:#fff
Common Misconception
✗ Mocking everything makes tests more isolated and therefore better. Over-mocking tests the mock configuration rather than real behaviour — tests pass even when the integration is broken. Mock at architectural boundaries (external services, databases) not at every class boundary.
Why It Matters
Mocks replace real dependencies so tests run fast and in isolation — a test that hits the database is slow, brittle, and tests more than one thing. Good mocking strategy is what makes a test suite actually fast enough to run on every commit.
Common Mistakes
- Mocking types you do not own (third-party classes) — create a wrapper interface and mock that instead.
- Over-mocking — if your test needs ten mocks to run, the design has too many dependencies.
- Not verifying mock interactions when the side effect is the point — e.g. asserting the mailer was called.
- Using mocks in integration tests — integration tests should use real (or in-memory) implementations.
Avoid When
- Mocking value objects or pure functions — these have no side effects and should be used directly in tests.
- Mocking the system under test itself — you end up testing your mock, not your code.
- Over-mocking to the point where the test passes even when the real implementation is broken.
- Mocking concrete classes with no interface — it couples the test to implementation details and breaks on refactoring.
When To Use
- Isolating the unit under test from slow or non-deterministic dependencies like databases, clocks, and HTTP clients.
- Verifying that the unit calls a dependency with the correct arguments — interaction testing.
- Simulating error conditions (network timeout, DB failure) that are hard to reproduce with real dependencies.
- Replacing third-party services in unit tests to avoid network calls, costs, and rate limits.
Code Examples
✗ Vulnerable
// Mock verifying calls instead of outcomes:
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())->method('send'); // Verifies send was called
// But: was the right email sent? To the right address? With the right content?
// Test passes even if the email body is empty or wrong
// Better: capture arguments and assert on the actual email content
✓ Fixed
// Using Mockery (more expressive than PHPUnit mocks)
use Mockery;
public function test_order_service(): void {
$mailer = Mockery::mock(MailerInterface::class);
$mailer->shouldReceive('send')
->once()
->with(Mockery::type(OrderConfirmation::class));
(new OrderService($mailer))->place($cart);
Mockery::close(); // or use MockeryPHPUnitIntegration trait
}
// Don't mock what you don't own — wrap 3rd-party libs
// Don't mock value objects — use real instances
// Prefer fakes for complex dependencies (in-memory repository)
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
15 Mar 2026
Edited
25 Mar 2026
Views
27
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
No pings yesterday
Amazonbot 8
Perplexity 6
Unknown AI 3
Ahrefs 2
SEMrush 2
Majestic 1
Google 1
Also referenced
How they use it
crawler 22
pre-tracking 1
Related categories
⚡
DEV INTEL
Tools & Severity
🟡 Medium
⚙ Fix effort: Medium
⚡ Quick Fix
Mock at the boundary (interface/abstract class), not the implementation — if you're mocking a concrete class, you probably need to extract an interface first
📦 Applies To
PHP 5.0+
web
cli
queue-worker
🔗 Prerequisites
🔍 Detection Hints
Mocking concrete classes; test with 10+ mock expectations; mock returning mocks (mock train wrecks)
Auto-detectable:
✗ No
phpunit
mockery
prophecy
⚠ Related Problems
🤖 AI Agent
Confidence: Low
False Positives: High
✗ Manual fix
Fix: Medium
Context: File
Tests: Update