Tests that run
without a harness to build.
Most of a backend is untestable until someone assembles a rig — a fake database, a request factory, a way to await a workflow. Voltro ships that rig. A handler is a function you call, a workflow runs on a real runner, and `voltro e2e` boots the whole app and tears it down again.
// A handler is a function — no server, no HTTP, no router mock
const result = await listNotes.handler({ tenantId: 'acme' }, ctx)
expect(result).toHaveLength(2)
// The in-memory store is the REAL interface, not a fake
const store = new InMemoryDataStore({ notes: [] })
// End to end: boots api + web, runs, tears both down
// $ voltro e2eThe pyramid, as shipped.
Handlers are just functions
A query, mutation or action is a descriptor plus a handler, so a unit test calls the handler with a context and asserts on the result. No HTTP, no server boot, no mocking a router — the fastest layer of the pyramid is the default one.
A store you do not have to fake
The in-memory data store implements the same interface the SQL stores do, so tests exercise the real query builder, the real mixin behaviour and the real change events. That matters because a hand-rolled fake only ever re-asserts its own return value — coverage theatre that passes while the thing it stands for is broken.
Workflows on a real runner
Durable workflows are tested by running them, not by asserting on a plan: steps execute, the journal is written, a signal can be delivered mid-run, and a replay is a replay. The suspend/resume path is where durable code actually breaks, so it is the path the tests take.
One command for end to end
`voltro e2e` boots the api and the web app, waits for readiness, runs your specs against the running pair, and tears both down — including on failure. The thing you test is the thing you deploy, not an approximation assembled in a config file.
Dialect parity, not dialect hope
Hand-written SQL is run against every supported dialect through a shared parity harness, so a statement that only works on Postgres fails in the suite rather than in a customer's MySQL. Suites skip loudly when a database is unreachable — a silent skip reads exactly like a pass.
Green means tests AND types
A test runner transpiles without typechecking, so a type error in a test file passes the suite and fails the build. `voltro typecheck` runs the app's own TypeScript over everything, test files included — the second half of "green" that is easy to skip and expensive to skip.
The failure mode we design against.
A suite that mocks its neighbour is a gap, not coverage
Two components that each mock the other produce a coverage number that reads as double and is a hole — the defect lives strictly between them and both suites stay green for its entire life. That is not a hypothetical: it cost this framework a control that never ran once in production, with no error and no log line. The lesson is baked into how the framework is tested — where two halves mock each other, one test wires the real things together, and it is written red first.
Three levels, one command each.
// tests/orders.test.ts
import { testApp } from '@voltro/testing'
const app = await testApp() // real store, no server, no mocks
const order = await app.mutation('orders.create', { total: 900 })
await expect(app.mutation('orders.ship', { id: order.id }))
.rejects.toMatchTag('NotPaid') // the TYPED error, by tagThe third is the one most stacks skip. Hand-written SQL is correct on the engine you develop against and quietly wrong on the one a customer runs, so the parity harness applies the same scenario to a real server per dialect — and reports the plan converging, not just the statements running.
The test story, in depth.
Why is most backend code hard to test, and what changes here?
Because testing it usually means assembling a rig first: a database that behaves like the real one, a way to construct a request, a way to await a background job. That rig is a project, so it gets deferred, and the tests that do exist end up asserting on mocks.
Here the rig ships. A handler is a function you call with a context — no HTTP, no server boot, no router to mock — and the in-memory store implements the same interface the SQL stores do, so a test exercises the real query builder, the real mixin behaviour and the real change events.
That last point is the one that matters most. A hand-rolled fake only ever re-asserts its own return value; it is coverage theatre that stays green while the thing it stands for is broken. Using the real interface with an in-memory store keeps the test fast without making it fictional.
How do you test something durable?
By running it. A workflow test executes on a real runner: steps run, the journal is written, a signal can be delivered mid-run, and a replay is a replay. Asserting on a plan instead would test the description rather than the behaviour.
That is deliberate because the suspend-and-resume path is exactly where durable code breaks. A workflow that works when nothing interrupts it is the easy case; the interesting one is what happens when the process dies between steps two and three.
Scheduled jobs get the same treatment, including the coordination that makes N replicas fire once. A test that only proves a single instance behaves correctly proves the case that was never in doubt.
What about the database itself?
Hand-written SQL runs against every supported dialect through a shared parity harness, so a statement that only works on Postgres fails in the suite rather than in someone else's MySQL a release later. Migration behaviour is verified by applying a real plan to a real database and asserting the rows survived AND that the next plan is empty.
That second assertion is the one people skip. Verifying statements is not verifying the plan: a migration that applies cleanly and then re-proposes itself forever is not converged, and it looks identical to success if you only check that the SQL ran.
Suites skip loudly when a database is unreachable. A silent skip reads exactly like a pass, which is how a suite comes to verify nothing while reporting green for months.
What does the framework do about tests that lie?
It treats specific failure shapes as known hazards. Two suites that each mock the other produce a coverage number that reads as double and is a hole — the defect lives strictly between them, and both stay green for its entire life. Where that pattern exists, one test wires the real things together, and it is written red first.
A test that measures the machine is another: asserting that two ticks happened after waiting sixty milliseconds is a claim about wall-clock, not about behaviour, and it fails under load while passing alone. Wait for the condition, not for a duration.
And a green suite is only half of green. A test runner transpiles without typechecking, so a type error in a test file passes the run and fails the build — which is why the typecheck is a separate step over the whole app, test files included.
The layers, and what each covers.
| Layer | Covers |
|---|---|
| Handler unit tests | Queries, mutations and actions called directly with a context — no HTTP, no server boot. |
| In-memory store | The real DataStore interface: real query builder, real mixin behaviour, real change events. |
| Workflow runner | Durable runs executed for real — journal, signals mid-run, suspend and resume, replay. |
| Dialect parity | Hand-written SQL and migration plans against every dialect; convergence asserted, not assumed. |
| voltro e2e | Boots api + web, waits for readiness, runs your specs, tears both down — including on failure. |
| voltro typecheck | The other half of green: tsc over the app including test files, which a test run does not do. |
Frequently asked questions
Do I need a database running to test?
Not for the fast layer. The in-memory store implements the same interface as the SQL ones, so handler tests exercise real behaviour without a server. A live database is needed for dialect parity and integration suites, which skip loudly when one is unreachable.
How do I test a workflow?
By running it on a real runner. Steps execute, the journal is written, and you can deliver a signal mid-run or assert that a replay resumes rather than restarts — the path where durable code actually breaks.
What does voltro e2e do?
Boots the api and the web app, waits for readiness, runs your specs against the running pair and tears both down, including on failure. You test the thing you deploy rather than an approximation assembled in a config file.
Is a passing test suite enough to merge?
No — a runner transpiles without typechecking, so a type error in a test file passes the suite and fails CI. Run the typecheck over the app with test files included; that is the half of green that is easy to skip and expensive to skip.
Do templates come with tests?
Yes, and they are gated: a harness scaffolds each template into a real workspace, installs it and runs its suite, with a separate typecheck pass. That gate has caught unbootable templates and APIs that no longer existed before any user scaffolded them.
What you end up testing.
Open the framework. See it for yourself.
Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.