Testing
Use @tyravel/testing with Vitest:
import { describe, it } from 'vitest';
import { TestCase } from '@tyravel/testing';
class FeatureTest extends TestCase {
protected override async setUp() {
await super.setUp();
// register providers, run migrations, etc.
}
}
describe('users', () => {
it('lists users', async () => {
const test = new FeatureTest();
await test.setUp();
const response = await test.get('/api/users');
response.assertStatus(200).assertJson({ ok: true });
await test.tearDown();
});
});HTTP test client
get,post,put,patch,delete— run throughHttpKernelwithToken('...')— attach Bearer tokenwithCsrf()— set session_csrf_tokenand sendX-CSRF-TOKEN(required for routes behindcsrfmiddleware)actingAs(user)/withSession({ ... })— inject auth and session state (requirescreateTestingMiddleware()on the app)- Cookie jar persists session cookies between requests
Routes using the csrf middleware reject POST requests without a matching token (HTTP 419). Chain withCsrf() before mutating requests:
await test.http.withCsrf().post('http://localhost/register', {
json: { name: 'Ada', email: '[email protected]', password: 'secret' },
});TestCase registers createTestingMiddleware() automatically. Custom test cases that override setUp() must call app.use(createTestingMiddleware()) before boot — see examples/hello-world/tests/support/reference-test-case.ts.
Assertions
response.assertStatus(200);
response.assertJson({ name: 'Ada' }); // partial match
response.assertJsonPath('data.0.id', 1);Container fakes
import { fake, mockInstance } from '@tyravel/testing';
fake('mail', { send: async () => {} });Wire facades to the test application with wireFacades(app) so Route, Auth, and Gate resolve correctly in tests.
Queued listeners and jobs
When tests use QUEUE_CONNECTION=database, queued listeners and mailables persist jobs until a worker processes them. After dispatching HTTP requests that trigger queued work, drain the queue before asserting side effects:
// See examples/hello-world/tests/support/reference-test-case.ts
await test.drainQueue();Use MAIL_MAILER=array (or fakes) together with queue draining to assert outbound mail in feature tests.
SSR and hydration assertions
When testing pages that use @island, capture the hydration manifest alongside the HTML:
const view = await renderView(app, 'welcome', { name: 'Ada' });
view.assertSee('Hello Ada');
view.assertIsland('counter');
view.assertHydrationManifest({
islands: [{ id: 'counter', html: expect.stringContaining('button'), props: { count: 0 } }],
});HTTP feature tests can assert the injected manifest on the full document:
const response = await test.get('/');
response.assertStatus(200);
response.assertSee('data-tyr-island="counter"');
response.assertSee('id="tyr-hydration"');Pest-style ergonomics
Import Vitest helpers and Tyravel lifecycle sugar from one module:
import { describe, expect, test, uses } from '@tyravel/testing/pest';
class FeatureTest extends TestCase {
protected createApplication() {
return new Application('/tmp/app');
}
}
const t = uses(FeatureTest);
describe('posts', () => {
test('lists posts', async () => {
await t.http.get('/posts').assertOk();
});
});uses() is an alias for withTyravelTest(). dataset() formats rows for test.each():
import { dataset, test } from '@tyravel/testing/pest';
test.each(dataset([
{ slug: 'draft', status: 201 },
{ slug: 'published', status: 200 },
]))('creates $slug', async ({ slug, status }) => {
// ...
});Parallel test runner (Vitest workspaces)
Large Tyravel apps benefit from Vitest workspaces so unit, feature, and package suites run in parallel without sharing one giant config.
Monorepo root — keep package tests isolated per project:
// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([
'packages/*',
'examples/*/vitest.config.ts',
]);Application — split fast unit tests from HTTP feature tests:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
pool: 'forks',
fileParallelism: true,
},
});// vitest.feature.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
name: 'feature',
include: ['tests/feature/**/*.test.ts'],
pool: 'forks',
fileParallelism: false, // one app boot at a time per worker
maxWorkers: 2,
},
});Register both in the workspace:
export default defineWorkspace([
'./vitest.config.ts',
'./vitest.feature.config.ts',
]);Guidelines for Tyravel feature tests:
- Prefer
uses(FeatureTest)/withTyravelTest()so each example gets a freshApplication. - Enable
usesDatabaseTransactionsonTestCasewhen tests touch SQLite/Postgres — avoids cross-test pollution when files run in parallel. - Keep
MAIL_MAILER=array,QUEUE_CONNECTION=sync(or fakes) in the test.envso parallel workers do not contend on shared mail/queue state. - Run
npm test -- --project featureto execute only the feature project in CI.