Testing
Preflow provides PHPUnit base cases for testing components, routes, and data layers. The testing package handles bootstrap, provides fluent assertions, and manages isolated storage for each test.
Installation
composer require preflow/testing --dev
ComponentTestCase
Extend ComponentTestCase to test Preflow components. A TestApplication is wired up automatically in setUp().
use Preflow\Testing\ComponentTestCase;
final class CounterTest extends ComponentTestCase
{
public function test_renders_count(): void
{
$html = $this->renderComponent(Counter::class, [
'initialCount' => 5,
]);
$this->assertStringContainsString('5', $html);
}
public function test_initial_state(): void
{
$counter = $this->createComponent(Counter::class, ['initialCount' => 0]);
$counter->resolveState();
$this->assertSame(0, $counter->count);
}
}
| Method | Description |
|---|---|
renderComponent(class, props) |
Full render lifecycle, returns HTML string |
createComponent(class, props) |
Creates component instance without rendering |
RouteTestCase and TestResponse
RouteTestCase provides an application instance. Wrap any PSR-7 response in TestResponse for fluent assertions:
use Preflow\Testing\RouteTestCase;
final class HealthTest extends RouteTestCase
{
public function test_health_endpoint(): void
{
$response = $this->app->handle($this->createRequest('GET', '/api/health'));
$this->createTestResponse($response)
->assertOk()
->assertJson()
->assertSee('"status":"ok"');
}
}
TestResponse Assertions
| Method | Description |
|---|---|
assertOk() |
Status 200 |
assertStatus(int) |
Exact status code |
assertSee(string) |
Body contains text |
assertNotSee(string) |
Body does not contain text |
assertRedirect(string) |
3xx status + matching Location header |
assertHeader(name, value) |
Exact header value |
assertHeaderContains(name, sub) |
Header contains substring |
assertJson() |
Content-Type contains "json" + valid JSON body |
assertForbidden() |
Status 403 |
assertNotFound() |
Status 404 |
assertUnauthorized() |
Status 401 |
All assertion methods return $this for chaining.
DataTestCase
DataTestCase provides isolated storage for each test: an in-memory SQLite connection and a temporary JSON directory, both torn down after each test.
use Preflow\Testing\DataTestCase;
use Preflow\Data\Migration\Table;
final class PostRepositoryTest extends DataTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createTable('posts', function (Table $table) {
$table->uuid('uuid')->primary();
$table->string('title');
$table->timestamps();
});
}
public function test_save_and_find(): void
{
$pdo = $this->getPdo();
$pdo->exec("INSERT INTO posts (uuid, title) VALUES ('1', 'Hello')");
$row = $pdo->query("SELECT * FROM posts WHERE uuid = '1'")->fetch();
$this->assertSame('Hello', $row['title']);
}
}
DataTestCase Methods
| Method | Returns |
|---|---|
getSqliteDriver() |
SqliteDriver (in-memory SQLite) |
getJsonDriver() |
JsonFileDriver (temp directory) |
dataManager() |
DataManager with both drivers registered |
createTable(name, callback) |
Creates a table via the Schema builder |
getPdo() |
Raw \PDO instance |
Auth Test Helpers
Test authenticated requests with the AuthTestHelpers trait:
use Preflow\Testing\AuthTestHelpers;
final class DashboardTest extends RouteTestCase
{
use AuthTestHelpers;
public function test_dashboard_requires_auth(): void
{
$user = new TestUser(uuid: 'u1', roles: ['admin']);
$request = $this->actingAs($user, $this->createRequest('GET', '/dashboard'));
$response = $this->app->handle($request);
$this->createTestResponse($response)->assertOk();
}
}
Running Tests
./vendor/bin/phpunit
Tests live in the tests/ directory. The skeleton includes example tests for components and routing to get you started.