Testing an application
The TestClient runs requests through the real kernel and middleware, with a separate in-memory session per client. It refuses applications whose environment is not explicitly testing. Create an isolated database and storage directory; the client does not clone or replace your database for you.
Working example
Install phpunit/phpunit:^12 as a development dependency. Save the complete example below as tests/ApiTest.php; run vendor/bin/phpunit --bootstrap vendor/autoload.php tests. In your application test factory, register your real routes and service providers on the isolated application so tests exercise your application behavior.
composer require --dev phpunit/phpunit:^12
<?php
declare(strict_types=1);
use Elang\Application;
use Elang\Testing\TestClient;
use PHPUnit\Framework\TestCase;
final class ApiTest extends TestCase
{
public function testReadyEndpoint(): void
{
$directory = sys_get_temp_dir().'/my-elang-test-'.bin2hex(random_bytes(8));
mkdir($directory, 0700, true);
try {
$app = new Application($directory, [
'environment' => 'testing',
'database' => ['url' => 'sqlite:///:memory:'],
'mail' => ['dsn' => 'null://null'],
'redis' => ['url' => ''],
'telemetry' => ['enabled' => false],
'operations' => ['enabled' => false],
]);
$app->router->get('/api/ready', fn () => ['ready' => true]);
$browser = new TestClient($app);
$browser->get('/api/ready')->assertOk()->assertJsonPath('ready', true);
} finally {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($files as $file) {
$file->isDir() && !$file->isLink()
? rmdir($file->getPathname()) : unlink($file->getPathname());
}
rmdir($directory);
}
}
}
Assertions and identities
Assertions include status, headers, redirects, visible text, JSON paths/counts and bound-value database checks. Use withSession() for test session values and actingAs($persistedUser) for authenticated requests. Session clients remain separate even when sharing an application.
CSRF remains active
CSRF is active. A protected write without a token should return 419. For a valid session write use $browser->withCsrf()->post(...); this supplies the real CSRF header and same-origin information instead of disabling middleware. Scope checks and record ownership still apply.
HTTP and mail fakes
The client replaces the injected Elang HTTP client with a strict FakeTransport and Symfony MailerInterface with an in-memory recorder. Configure $browser->http->when(...), assert HTTP requests, and inspect $browser->mail->messages() or assertSentCount/assertSentTo. Fakes are shared between clients using the same application. Direct cURL calls, independently constructed mail transports and unrelated SDKs are outside this interception; replace those adapters explicitly. Never point application tests at a live database.
Try it together
Run the complete catalogue example from an application with the optional ORM installed. It uses a disposable in-memory database and a fake partner API. Expected output includes "checks":"passed". The example covers entity relationships, fetch joins, resources, collections and HTTP/kernel assertions.
php vendor/elang/framework/examples/orm-catalog/run.php
Next step · GitHub: complete runnable source
Common mistakes
| Symptom | Fix |
|---|---|
| Protected write returns 419 | Assert the rejection or use withCsrf() for a valid same-origin session write. |
| Another SDK still contacts the network | Replace that adapter explicitly; only the injected Elang client and mailer are faked. |