Skip to content

Unit Testing

Unit tests verify individual classes in isolation using Pest PHP, a testing framework with expressive functional syntax.

Basic Structure

test('Title can be created from valid string', function (): void {
    $title = Title::fromString('Clean Code');
    expect($title->toString())->toBe('Clean Code');
});

test('Title 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

test('Title 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 -- --filter="Title"          # Filter by name
task tests:unit -- tests/Phexium/Unit/Domain/   # Specific directory

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:

// Thresholds per test suite
$slowTestThreshold = match ($suite ?? null) {
    'Unit' => 20,         // 20ms for unit tests
    'Integration' => 200, // 200ms for integration tests
    default => 500,       // 500ms for other tests
};

Slow tests appear in yellow at the end of the test output:

⚠ Detected 3 slow tests (threshold: 20ms):
  45ms It validates complex ISBN checksum
  32ms It handles large collection operations
  25ms It processes multiple events

SlowTestCollector

The collector uses Pest hooks to measure test duration:

final class SlowTestCollector
{
    public static function add(string $name, float $durationMs): void;
    public static function report(): void;
}

uses()
    ->beforeEach(function (): void {
        $this->slowTestStartTime = hrtime(true);
    })
    ->afterEach(function (): void {
        $durationMs = (hrtime(true) - $this->slowTestStartTime) / 1_000_000;

        if ($durationMs > SLOW_TEST_THRESHOLD_MS) {
            SlowTestCollector::add($this->name(), $durationMs);
        }
    })
    ->in(__DIR__);

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