Unit Testing
Unit tests verify individual classes in isolation using Pest PHP, a testing framework with expressive functional syntax.
Basic Structure
describe('Creation', function (): void {
it('creates a title from valid string', function (): void {
$title = Title::fromString('Clean Code');
expect($title->toString())->toBe('Clean Code');
});
it('rejects empty string', function (): void {
expect(fn () => Title::fromString(''))
->toThrow(InvalidArgumentException::class);
});
});
Expectations
expect($value)->toBe($expected); // Strict ===
expect($value)->toBeTrue();
expect($value)->toBeInstanceOf(Book::class);
expect($array)->toHaveCount(3);
expect($string)->toContain('substring');
Chained Expectations
Exception Testing
expect(fn () => Title::fromString(''))
->toThrow(InvalidArgumentException::class, 'Expected message');
Data-Driven Tests
it('accepts valid strings', function (string $input): void {
$title = Title::fromString($input);
expect($title->toString())->toBe($input);
})->with(['Clean Code', 'Domain-Driven Design', 'A']);
What to Unit Test
- Value Objects: Validation, equality, behavior
- Entities: Domain logic, state transitions
- Handlers: Business rules with mocked dependencies
Running Tests
task tests:unit # All unit tests
task tests:unit:phexium # Phexium only
task tests:unit:appdemo # AppDemo only
task tests:unit -- --filter="Title" # Filter by name
Pest Configuration
The tests/Pest.php file configures the test environment with custom features.
Slow Test Detection
Tests exceeding a configurable threshold are collected and reported at the end of the test run. The threshold is determined per-test based on its Pest group (unit, integration):
define('SLOW_TEST_THRESHOLDS', [
'unit' => 20, // 20ms for unit tests
'integration' => 200, // 200ms for integration tests
]);
define('SLOW_TEST_DEFAULT_THRESHOLD_MS', 500);
Slow tests appear in yellow at the end of the test output:
⚠ Detected 3 slow tests:
45ms (threshold: 20ms) It validates complex ISBN checksum
32ms (threshold: 20ms) It handles large collection operations
25ms (threshold: 20ms) It processes multiple events
Environment Loading
The .env file is loaded automatically for tests:
Best Practices
- One behavior per test
- Descriptive test names
- Arrange-Act-Assert pattern
- Prefer InMemory implementations over mocks
- No logic in tests (no if/loops)
- Keep unit tests under 20ms threshold
See Also
- Value Objects - Primary unit test target
- Entities - Entity behavior testing
- Commands & Handlers - Handler testing with mocks