Mocking Best Practices (PHPUnit & Mockery)
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints explicitly state automated=no, and while tools like phpunit, mockery, and prophecy are listed, none of them automatically flag over-mocking, mock trains, or mocking concrete classes as anti-patterns. A test with 10+ mock expectations or mock train wrecks requires a human reviewer to recognize the smell — no linter or static tool will catch it reliably.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix states 'if you're mocking a concrete class, you probably need to extract an interface first,' which means the fix isn't a one-line swap — it requires introducing a new interface, updating the production code to depend on it, and updating the mock in tests. Over-mocking patterns may require redesigning dependency structure across a component, touching multiple files.
Closest to 'persistent productivity tax' (b5). The applies_to spans web, cli, and queue-worker contexts, meaning bad mocking habits permeate all PHP contexts in a project. Over-mocking or mocking concrete classes spreads across the test suite and slows many work streams — every new test written must navigate the established patterns. However, it doesn't fully define the system's shape (b7), so b5 is appropriate.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field explicitly names the trap: 'Mocking everything makes tests more isolated and therefore better.' This is a widely held and intuitive belief — isolation sounds like it should be better — yet it directly contradicts the actual outcome where over-mocked tests pass even when real integrations are broken. This is a well-documented but deeply counterintuitive gotcha that competent developers routinely fall into.
Also Known As
TL;DR
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
Why It Matters
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
// 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
// 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)