Skip to content

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

expect($book)
    ->toBeInstanceOf(Book::class)
    ->and($book->title()->toString())->toBe('Clean Code');

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:

$dotenv = Dotenv::createImmutable(__DIR__.'/..', '.env');
$dotenv->safeLoad();

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