Skip to content

Testing Strategy Overview

Phexium uses a multi-layer testing strategy with three test types: Unit, Integration, and Acceptance.

Test Pyramid

         /\
        /  \
       / AT \      Acceptance Tests (Behat)
      /------\         → end-to-end user scenarios
     /   IT   \    Integration Tests (Pest)
    /----------\       → component interactions
   /     UT     \  Unit Tests (Pest)
  /--------------\     → isolated logic testing

Test Types

Unit Tests

Unit tests verify individual components in isolation.

Location: tests/Phexium/Unit/, tests/AppDemo/Unit/

Scope: Value Objects, Entities, Handlers, Collections

task tests:unit

Integration Tests

Integration tests verify component interactions with real dependencies.

Location: tests/Phexium/Integration/, tests/AppDemo/Integration/

Scope: Repository implementations, Bus dispatching, Middleware chains

task tests:integration

Acceptance Tests

Acceptance tests verify complete user scenarios with Behat (BDD).

Location: tests/AppDemo/Acceptance/, tests/AppStarter/Acceptance/

Scope: Full HTTP request/response cycles, User workflows

task tests:acceptance                    # All databases
task tests:acceptance:demoInMemory       # InMemory only
task tests:acceptance:demoSqlite         # SQLite only

Test Framework

Phexium uses Pest PHP for unit and integration tests:

test('Title rejects empty string', function (): void {
    expect(fn () => Title::fromString(''))
        ->toThrow(InvalidArgumentException::class);
});

Running All Tests

task tests:all

This runs unit, integration, and acceptance tests with coverage merge.

Mutation Testing

Verify test quality by introducing code mutations:

task tests:mutation

See Mutation Testing for detailed guidance.

Test-Driven Development (TDD)

TDD is the recommended approach: write tests first, then implement. Follow the Red-Green-Refactor cycle.

The TDD Cycle

┌─────────────────────────────────────────────┐
│                                             │
│   ┌───────┐    ┌───────┐    ┌──────────┐   │
│   │  RED  │───▶│ GREEN │───▶│ REFACTOR │   │
│   └───────┘    └───────┘    └──────────┘   │
│       ▲                           │        │
│       └───────────────────────────┘        │
│                                             │
└─────────────────────────────────────────────┘

1. Red - Write Failing Test

  • Write a test for the next small piece of functionality
  • Test should fail (no implementation yet)
  • Test describes expected behavior

2. Green - Make It Pass

  • Write minimal code to make the test pass
  • Don't over-engineer
  • Just enough to satisfy the test

3. Refactor - Improve

  • Clean up code while tests remain green
  • Remove duplication
  • Improve naming
  • No new functionality

TDD Example

// 1. RED - Write test first
test('Title rejects empty string', function (): void {
    expect(fn () => Title::fromString(''))
        ->toThrow(InvalidArgumentException::class);
});
// Run: FAIL (Title class doesn't exist)

// 2. GREEN - Minimal implementation
final readonly class Title
{
    private function __construct(private string $value)
    {
        if ($value === '') {
            throw new InvalidArgumentException('Title cannot be empty');
        }
    }

    public static function fromString(string $value): self
    {
        return new self($value);
    }
}
// Run: PASS

// 3. REFACTOR - Improve (add trim, use Assertion library)

TDD Benefits

  • Tests document behavior - Tests serve as living documentation
  • Design emerges from usage - API designed from consumer perspective
  • High coverage by default - Every feature has tests
  • Confidence in refactoring - Tests catch regressions immediately

When to Use TDD

Use TDD Skip TDD (add tests later)
New features Exploratory/spike code
Bug fixes (reproduce first) Infrastructure wiring
Value Objects and Entities Framework configuration
Business logic in handlers Proof of concept
Refactoring existing code

Common TDD Mistakes

  • Writing tests after implementation (loses design benefits)
  • Testing implementation details instead of behavior
  • Too many assertions per test (hard to diagnose failures)
  • Skipping the refactor step (accumulates technical debt)
  • Not running tests frequently (delays feedback)

Test Granularity

  • One assertion per test (preferred) - Clear failure diagnosis
  • Test one behavior at a time - Focused, maintainable tests
  • Small, focused tests - Fast execution, easy to understand
  • Descriptive test names - Document expected behavior

See Also