← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Mocking Best Practices (PHPUnit & Mockery)

testing PHP 5.0+ Intermediate

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)

Added 15 Mar 2026
Edited 25 Mar 2026
Views 27
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 2 pings F 1 ping S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 0 pings T 2 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Amazonbot 8 Perplexity 6 Unknown AI 3 Ahrefs 2 SEMrush 2 Majestic 1 Google 1
crawler 22 pre-tracking 1
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

✓ schema.org compliant