PHP 8: Modern PHP Development · Testing and Tooling

Test Doubles and Integration Tests

Use stubs, mocks and fakes wisely, and test database code against a real database.

PHPUnit's createStub() returns canned values and createMock() also verifies calls; libraries like Mockery and Prophecy are alternatives. Prefer fakes (in-memory repositories) over mocking everything. For database code, write integration tests against the real engine (Docker/Testcontainers or a CI service) and wrap each test in a transaction that rolls back. Use framework test clients (Symfony WebTestCase, Laravel HTTP tests) for request-level tests.

<?php
final class RegisterUserTest extends TestCase
{
    public function test_it_dispatches_user_registered(): void
    {
        $users = new InMemoryUserRepository();                    // fake
        $hasher = $this->createStub(PasswordHasher::class);       // stub
        $hasher->method('hash')->willReturn('hashed');
        $events = $this->createMock(EventDispatcherInterface::class);   // mock
        $events->expects($this->once())->method('dispatch')->with($this->isInstanceOf(UserRegistered::class));

        (new RegisterUser($users, $hasher, $events))('[email protected]', 'secret-password');

        $this->assertNotNull($users->findByEmail('[email protected]'));
    }
}

abstract class DatabaseTestCase extends TestCase
{
    protected PDO $pdo;
    protected function setUp(): void { $this->pdo = createPdo(); $this->pdo->beginTransaction(); }
    protected function tearDown(): void { $this->pdo->rollBack(); }
}

Common pitfalls

  • Mocking types you don't own (e.g., PDO) instead of wrapping them.
  • Testing PostgreSQL code against SQLite.
  • Over-specified mocks that break on harmless refactors.

Key points

  • Stubs return data; mocks verify calls
  • Fakes for repositories
  • Real database for integration tests

Further reading: PHPUnit: test doubles

Practice questions

Why avoid mocking PDO directly?

Answer

Mocking third-party types couples tests to their API details and doesn't verify real SQL. Wrap persistence in your own repository interface and test the PDO implementation against a real database.

How can integration tests share a database without interfering?

Answer

Run each test inside a transaction that's rolled back in tearDown(), or truncate tables between tests, and avoid relying on test order.

Open in the app to mark lessons complete, take notes and track your progress.