Writing Tests
Now that you know how the harness runs, let us write tests with it. A test file is just PHP that returns a closure, the closure registers named tests on a Suite, and each test uses the Assert helpers to state what should be true.
The shape of a test file
For pure code, a test file returns a function that takes a Suite and registers one or more tests on it:
<?php
use Fast\Tests\Harness\Assert;
use Fast\Tests\Harness\Suite;
return function (Suite $suite): void {
$suite->test('validated input is normalized', function (): void {
$data = validate(['age' => '42'], ['age' => 'required|int']);
Assert::eq(42, $data['age']);
});
};
Each test() gets a descriptive name and a closure body. The name is what you match with --filter, so make it readable.
The Assert helpers
Assert covers the checks you reach for most. The core ones:
Assert::eq($got, $want); // equal
Assert::ne($a, $b); // not equal
Assert::true($condition);
Assert::false($condition);
Assert::null($value);
Assert::multisetEq($a, $b); // same elements, order-independent
For error paths, throws() asserts that a callable throws (optionally of a specific class) and returns the exception so you can inspect it. Its counterpart noThrow() asserts a callable runs cleanly and returns its result:
Assert::throws(
fn () => validate([], ['email' => 'required|email']),
Fast\Validation\ValidationException::class,
);
$result = Assert::noThrow(fn () => parse_config($input));
And when a test cannot run because a dependency is missing, skipUnless() marks it skipped rather than failed:
Assert::skipUnless(extension_loaded('pdo_pgsql'), 'pgsql driver not installed');
Property tests
Beyond example-based tests, the suite supports property tests that run a closure over many generated inputs, which is great for finding edge cases you would not think to write by hand:
$suite->prop('escaping is idempotent for safe text', function (string $s): void {
Assert::eq(e($s), e($s));
}, iterations: 500);
A property failure prints the seed, so you can replay the exact input with --seed=N.
Testing HTTP behavior
For anything involving the real HTTP wire, boot a fixture server through Fast\Tests\Harness\HttpServer, send it raw requests, and assert on the responses. Always stop it in a finally so it never leaks:
$suite->test('root route responds', function (): void {
$server = new Fast\Tests\Harness\HttpServer(/* ... */);
try {
$response = $server->raw("GET / HTTP/1.1\r\nHost: x\r\n\r\n");
Assert::true(str_contains($response, '200'));
} finally {
$server->stop();
}
});
HttpServer binds an ephemeral port you read with port(), and gives you raw() for a single request, pipeline() for several, and lower-level open()/readRaw() when you need fine control.
Write tests against the contract, not the code
The most valuable habit in testing Fast apps is to write your assertions from the documented behavior, not by mirroring the implementation:
Prefer, in order: differential comparisons against an independent oracle, properties and invariants, metamorphic relationships, adversarial and fuzz inputs, and only then exact examples, reserved for when the precise bytes or value are the public contract. Do not rewrite an expected result just to match what the code currently does, and do not weaken a real contract to make a test go green. A test that only restates the implementation cannot catch the implementation being wrong.
Favor small, semantic assertions over broad snapshots: check the status, the header, the state change, the ordering guarantee, or the resource release explicitly, rather than diffing a whole blob of HTML or JSON.
That completes the documentation. You now have the full tour of Fast, from your first route to a hardened, tested deployment. Happy building!