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.