The Test Harness

Fast ships its own dependency-free test harness, so you can verify your application and Fast's own behavior without pulling in PHPUnit or anything else. It is small, fast, and built around the same no-dependency philosophy as the rest of the framework.

Running tests

Tests run through a single runner script. The basic invocations:

php tests/run.php                    # the deterministic suite
php tests/run.php --filter=Routing   # only tests whose name matches
php tests/run.php --seed=123         # replay a randomized run
php tests/run.php --integration      # include integration tests
php tests/run.php --coverage         # with coverage

There are also dedicated runners for memory and benchmarks:

php tests/memory.php
php tests/memory.php --soak --advisory
php tests/bench.php

Before running the suite, a quick syntax check on the files you changed catches the obvious mistakes:

php -l path/to/file.php

Two tiers: deterministic and integration

The harness splits tests into two tiers, and choosing the right one keeps your suite both meaningful and fast.

Deterministic tests are for logic that does not need real sockets or processes: routing and response contracts, validation, query compilation and model hydration, session and auth helpers with controlled dependencies, and pure domain services. They run quickly and repeatably.

Integration tests are for behavior that genuinely crosses process or socket boundaries: real HTTP wire behavior, worker lifecycle, actual public sockets, proc and prefork behavior, database executor death and replacement, and cross-worker transport. They are slower but they exercise the real machine.

A good rule: reach for the deterministic tier by default, and only escalate to integration when the thing you are testing truly involves a socket, a process, or a transport. Most of your tests should be deterministic.

Use real SQLite for SQL

When you test anything that touches SQL, migrations, transactions, model persistence, query semantics, use a real SQLite database rather than a fake. A mock will happily accept SQL that a real database would reject, so it gives you false confidence. Real SQLite catches genuine schema and constraint problems.

For MySQL or PostgreSQL specifics, tests should report SKIPPED (not passed) when the driver or the relevant DSN environment variable is not available, so a missing database never masquerades as a passing test.

Reproducibility

The harness is designed so that failures are reproducible. Randomized tests print a seed on failure, which you replay with --seed=N. Things that genuinely cannot be pinned to a golden value, OS scheduling, process timing, encryption nonces, object ids, are tested by invariant instead of by exact match.

For any test that waits on a socket or a process, always add a bounded deadline, use ephemeral 127.0.0.1:0 ports rather than a fixed one, track and terminate the exact child PIDs you spawn, and clean up temporary databases, sockets, and directories in a finally block. These habits keep the suite deterministic and prevent leaked resources from poisoning later tests.

Verification order

When you change something, a sensible progression is:

  1. php -l the changed PHP files.
  2. Run the narrowest matching --filter.
  3. Run the full deterministic tier.
  4. Add --integration if your change crosses sockets or processes.
  5. Run memory or benchmark gates only when the change can affect lifecycle,

allocation, or a hot path. Keep coverage and benchmark runs separate, since instrumentation changes timing.

With the harness understood, let us write some actual tests in Writing Tests.