| title | PHP Testing Requirements |
|---|---|
| scope | php |
- Minimum 80% line coverage for new code
- Critical paths (authentication, payment, data mutation) require 95%+ coverage
- Use PCOV for fast coverage collection; Xdebug for debugging only
- PHPUnit 10+ as the primary test framework
- Pest 2+ as an alternative (especially for Laravel projects)
- Do not mix PHPUnit and Pest in the same project without team agreement
- Test classes:
{ClassName}Testin a mirrored directory structure - Test methods:
test_it_does_somethingorit('does something')in Pest - Use descriptive names that explain the behavior, not the implementation
tests/
├── Unit/ # Isolated tests, no I/O
├── Integration/ # Tests with real dependencies (DB, filesystem)
├── Functional/ # End-to-end feature tests
└── Fixtures/ # Shared test data
- Arrange-Act-Assert — clear separation in every test
- One assertion per test concept (multiple assertions are okay if testing the same behavior)
- Use data providers for testing multiple inputs:
#[DataProvider('validEmailProvider')]
public function test_it_validates_email(string $email, bool $expected): void
{
$this->assertSame($expected, $this->validator->isValid($email));
}
public static function validEmailProvider(): iterable
{
yield 'standard email' => ['user@example.com', true];
yield 'missing @' => ['userexample.com', false];
yield 'empty string' => ['', false];
}- Use PHPUnit's built-in mock builder for simple cases
- Use Mockery for complex mock scenarios
- Never mock value objects or DTOs — use real instances
- Mock at boundaries: HTTP clients, database connections, external services
- Prefer fakes and stubs over mocks when possible
- Use transactions that rollback after each test
- Use factories for test data generation
- Never rely on specific database IDs
- Test against the same database engine used in production
- Public API of each class
- Edge cases: empty inputs, boundary values, null handling
- Error paths: exceptions, validation failures
- Integration points: database queries, API calls, file operations
- Private methods directly (test through public API)
- Framework internals
- Third-party library behavior
- Trivial getters/setters without logic