@avelon/assay
@avelon/assay is the test harness. It sits on bun test and gives you HTTP helpers that dispatch through the core kernel, model factories, and seeds. Reach for it when a feature test should speak in requests, factories, and assertions rather than constructing HttpRequest by hand.
Assay tests a material for what it really is: the same kernel, routes, and models the adapter will serve.
Installation
bun add -d @avelon/assay
Assay expects @avelon/core and @avelon/orm. You pass a kernel and route table in; vendor drivers stay in test setup, never in app/.
Basic Usage
import { FakeDatabase } from '@avelon/conformance'
import { createKernel, defineConfig, view } from '@avelon/core'
import { assay } from '@avelon/assay'
defineConfig({ name: 'tests', drivers: { database: new FakeDatabase() } })
const kernel = createKernel()
const client = assay({ kernel, routes })
const index = await client.get('/users')
index.assertOk().assertView('users.index')
const stored = await client.actingAs({ id: 'u1' }).post('/users', { name: 'Ada' })
stored.assertRedirect('/users/u1')
HTTP Helpers
assay() matches method plus path against a RouteDefinition table, preferring static segments so /users/create does not lose to /users/{user}. Each helper builds an HttpRequest, dispatches through the kernel, and flushes after listeners.
const shown = await client.get('/users/u1')
shown.assertStatus(200)
shown.view().props
call(name, httpRequest()) dispatches a named route when you already have params. Unauthenticated S4-style kernels still read cookies.user; actingAs('ada') writes that cookie.
Factories
Factories fill Scrivener models. Sequence numbers start at 1. There is no bundled faker; you return plain attributes so tests stay deterministic.
import { defineFactory } from '@avelon/assay'
import { User } from '@/app/Models/User'
const UserFactory = defineFactory(User, (sequence) => ({
id: `user-${sequence}`,
email: `user-${sequence}@example.test`,
name: `User ${sequence}`,
}))
UserFactory.state('ada', () => ({ name: 'Ada Lovelace', email: 'ada@example.test' }))
await UserFactory.make()
await UserFactory.create({ name: 'Ada' })
await UserFactory.as('ada').create()
await UserFactory.createMany(3)
Seeds
Seeds are ordinary modules with a default export. reeve db:seed and runSeeds(dir) load database/seeds in filename order.
import { defineSeed } from '@avelon/assay'
export default defineSeed(async () => {
await UserFactory.createMany(5)
}, 'users')
import { runSeed, runSeeds } from '@avelon/assay'
await runSeed(seed)
await runSeeds('database/seeds')
Method Reference
| Method / export | Signature | Description | |
|---|---|---|---|
assay | (options: AssayOptions) => AssayClient | Creates an HTTP client bound to a kernel and route table. | |
AssayClient.get | (path, headers?) => Promise<AssayResponse> | Dispatches GET. | |
AssayClient.post | (path, body?, headers?) => Promise<AssayResponse> | Dispatches POST. | |
AssayClient.put | (path, body?, headers?) => Promise<AssayResponse> | Dispatches PUT. | |
AssayClient.patch | (path, body?, headers?) => Promise<AssayResponse> | Dispatches PATCH. | |
AssayClient.delete | (path, headers?) => Promise<AssayResponse> | Dispatches DELETE. | |
AssayClient.call | (name, request) => Promise<AssayResponse> | Dispatches a named route with an existing HttpRequest. | |
AssayClient.actingAs | `(actor: { id: string } \ | string) => this` | Sets the actor cookie for subsequent requests. |
AssayClient.asGuest | () => this | Clears actor cookies. | |
AssayResponse.assertOk | () => this | Fails unless the outcome is 2xx and not a failed action. | |
AssayResponse.assertStatus | (expected: number) => this | Fails unless the status hint equals expected. | |
AssayResponse.assertRedirect | (location: string) => this | Fails unless the kernel redirected to location. | |
AssayResponse.assertView | (view: unknown) => this | Fails unless the kernel rendered view. | |
AssayResponse.status | () => number | Returns the transport-neutral status hint. | |
AssayResponse.view | () => ViewResult | Returns the view result or throws. | |
AssayResponse.data | () => unknown | Returns action data or view props. | |
AssayResponse.result | KernelResult | Underlying kernel result. | |
AssayAssertion | class AssayAssertion extends Error | Thrown when an assertion does not hold. | |
matchRoute | (routes, method, path) => { route, params } | Resolves a path, preferring static segments. | |
httpRequest | (overrides?) => HttpRequest | Builds a kernel request with test defaults. | |
parsePath | (path: string) => { pathname, query } | Splits path and query, preserving repeated keys. | |
requestFromCall | (options) => HttpRequest | Builds a request for an HTTP helper call. | |
encodeBody | (body: unknown) => Uint8Array | Encodes a body for rawBody(). | |
defineFactory | (model, definition) => Factory | Creates a model factory. | |
Factory.make | (overrides?) => Promise<TModel> | Builds a model without persisting. | |
Factory.create | (overrides?) => Promise<TModel> | Persists one model. | |
Factory.createMany | (count, overrides?) => Promise<readonly TModel[]> | Persists count models. | |
Factory.state | (name, attributes) => this | Registers a named attribute overlay. | |
Factory.as | (name: string) => this | Applies a named state to the next make/create. | |
Factory.reset | () => void | Clears sequence and pending states. | |
Factory.sequence | number | Current sequence number. | |
defineSeed | (run, name?) => Seed | Wraps a seed callback. | |
runSeed | (seed, context?) => Promise<void> | Runs one seed. | |
runSeeds | (dir: string) => Promise<readonly string[]> | Imports and runs default exports in filename order. | |
AssayOptions | interface | Kernel, routes, and optional actor cookie name. | |
Seed | interface | Named seed with a run callback. | |
FactoryDefinition | type | (sequence) => attributes factory callback. | |
FactoryState | type | () => attributes overlay registered with Factory.state. | |
SeedCallback | type | `(context: SeedContext) => Promise<void> \ | void` seed body. |
SeedContext | interface | Optional path of a seed file loaded from disk. |
Testing
Assay is itself tested with FakeDatabase and createKernel. Point assay() at the same kernel your adapter mounts.
import { assay } from '@avelon/assay'
import { FakeDatabase } from '@avelon/conformance'
const client = assay({ kernel, routes })
await client.get('/users').assertOk()
bun test
bun run typecheck