Tests, die laufen —
ohne erst ein Harness zu bauen.

Der größte Teil eines Backends ist untestbar, bis jemand eine Vorrichtung baut: eine gefakte Datenbank, eine Request-Factory, irgendeinen Weg, auf einen Workflow zu warten. Voltro liefert diese Vorrichtung mit. Ein Handler ist eine Funktion, die du aufrufst, ein Workflow läuft auf einem echten Runner, und `voltro e2e` fährt die ganze App hoch und wieder herunter.

notes.test.ts
TypeScript
// 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 e2e

Die Pyramide, wie ausgeliefert.

Handler sind einfach Funktionen

Eine Query, Mutation oder Action ist ein Descriptor plus ein Handler — ein Unit-Test ruft den Handler mit einem Context auf und prüft das Ergebnis. Kein HTTP, kein Server-Boot, kein gemockter Router: die schnellste Schicht der Pyramide ist die Standardschicht.

Ein Store, den du nicht faken musst

Der In-Memory-Store implementiert dieselbe Schnittstelle wie die SQL-Stores — Tests fahren also den echten Query-Builder, das echte Mixin-Verhalten und die echten Change-Events. Das zählt, weil ein handgeschriebener Fake am Ende nur seinen eigenen Rückgabewert bestätigt: Coverage-Theater, das grün ist, während das Gemeinte kaputt ist.

Workflows auf einem echten Runner

Durable Workflows werden getestet, indem man sie laufen lässt, nicht indem man einen Plan prüft: Steps führen aus, das Journal wird geschrieben, ein Signal kann mitten im Lauf ankommen, und ein Replay ist ein Replay. Der Suspend/Resume-Pfad ist der, an dem durabler Code wirklich bricht — also der Pfad, den die Tests nehmen.

Ein Befehl für End-to-End

`voltro e2e` fährt api und Web-App hoch, wartet auf Readiness, fährt deine Specs gegen das laufende Paar und baut beides wieder ab — auch im Fehlerfall. Was du testest, ist das, was du deployst, keine in einer Config-Datei zusammengesetzte Näherung.

Dialekt-Parität statt Dialekt-Hoffnung

Handgeschriebenes SQL läuft über ein gemeinsames Paritäts-Harness gegen jeden unterstützten Dialekt — ein Statement, das nur auf Postgres funktioniert, scheitert also in der Suite statt im MySQL eines Kunden. Suites überspringen laut, wenn eine Datenbank nicht erreichbar ist: ein stiller Skip liest sich exakt wie ein Pass.

Grün heißt Tests UND Typen

Ein Test-Runner transpiliert, ohne zu typechecken — ein Typfehler in einer Testdatei besteht also die Suite und kippt den Build. `voltro typecheck` fährt das TypeScript der App über alles, Testdateien eingeschlossen: die zweite Hälfte von „grün", die leicht zu überspringen und teuer zu überspringen ist.

Der Fehlermodus, gegen den wir entwerfen.

Eine Suite, die ihren Nachbarn mockt, ist eine Lücke, keine Abdeckung

Zwei Komponenten, die sich gegenseitig mocken, erzeugen eine Coverage-Zahl, die sich doppelt liest und ein Loch ist — der Defekt lebt genau dazwischen, und beide Suites bleiben sein ganzes Leben lang grün. Das ist nicht hypothetisch: es hat dieses Framework eine Steuerung gekostet, die in Produktion kein einziges Mal lief, ohne Fehler und ohne Logzeile. Die Lehre steckt in der Art, wie das Framework getestet wird — wo zwei Hälften einander mocken, verdrahtet ein Test die echten Dinge, und er wird zuerst rot geschrieben.

Drei Ebenen, je ein Befehl.

tests/orders.test.ts
ts
// 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 tag

Die dritte lassen die meisten Stacks aus. Handgeschriebenes SQL ist auf der Engine korrekt, gegen die du entwickelst, und still falsch auf der, die ein Kunde betreibt — die Paritäts-Harness fährt dasselbe Szenario deshalb pro Dialekt gegen einen echten Server und prüft, dass der Plan konvergiert, nicht nur dass die Statements durchlaufen.

Die Test-Geschichte im Detail.

Warum ist Backend-Code meist schwer testbar, und was ändert sich hier?

Weil ihn zu testen normalerweise heißt, zuerst eine Vorrichtung zu bauen: eine Datenbank, die sich wie die echte verhält, einen Weg, einen Request zu konstruieren, einen Weg, auf einen Hintergrundjob zu warten. Diese Vorrichtung ist ein Projekt, also wird sie verschoben — und die Tests, die es gibt, prüfen am Ende Mocks.

Hier kommt die Vorrichtung mit. Ein Handler ist eine Funktion, die du mit einem Context aufrufst — kein HTTP, kein Server-Boot, kein gemockter Router — und der In-Memory-Store implementiert dieselbe Schnittstelle wie die SQL-Stores, ein Test fährt also den echten Query-Builder, das echte Mixin-Verhalten und die echten Change-Events.

Der letzte Punkt zählt am meisten. Ein handgeschriebener Fake bestätigt am Ende nur seinen eigenen Rückgabewert; er ist Coverage-Theater, das grün bleibt, während das Gemeinte kaputt ist. Die echte Schnittstelle mit einem In-Memory-Store zu nutzen hält den Test schnell, ohne ihn fiktiv zu machen.

Wie testet man etwas Durables?

Indem man es laufen lässt. Ein Workflow-Test führt auf einem echten Runner aus: Schritte laufen, das Journal wird geschrieben, ein Signal kann mitten im Lauf ankommen, und ein Replay ist ein Replay. Stattdessen einen Plan zu prüfen würde die Beschreibung testen statt das Verhalten.

Das ist Absicht, denn der Suspend-und-Resume-Pfad ist genau die Stelle, an der durabler Code bricht. Ein Workflow, der funktioniert, wenn ihn nichts unterbricht, ist der leichte Fall; der interessante ist, was passiert, wenn der Prozess zwischen Schritt zwei und drei stirbt.

Geplante Jobs werden genauso behandelt, inklusive der Koordination, die N Replicas einmal feuern lässt. Ein Test, der nur beweist, dass eine einzelne Instanz sich korrekt verhält, beweist den Fall, der nie in Zweifel stand.

Was ist mit der Datenbank selbst?

Handgeschriebenes SQL läuft über ein gemeinsames Paritäts-Harness gegen jeden unterstützten Dialekt — ein Statement, das nur auf Postgres funktioniert, scheitert also in der Suite statt ein Release später im MySQL von jemand anderem. Migrationsverhalten wird geprüft, indem ein echter Plan auf eine echte Datenbank angewandt wird und geprüft wird, dass die Zeilen überlebt haben UND der nächste Plan leer ist.

Die zweite Prüfung ist die, die übersprungen wird. Statements zu verifizieren ist nicht dasselbe wie den Plan zu verifizieren: eine Migration, die sauber durchläuft und sich danach ewig selbst neu vorschlägt, ist nicht konvergiert — und sie sieht aus wie Erfolg, wenn man nur prüft, dass das SQL lief.

Suites überspringen laut, wenn eine Datenbank nicht erreichbar ist. Ein stiller Skip liest sich exakt wie ein Pass — so kommt es, dass eine Suite monatelang nichts verifiziert und dabei grün meldet.

Was tut das Framework gegen Tests, die lügen?

Es behandelt bestimmte Fehlerformen als bekannte Gefahren. Zwei Suites, die sich gegenseitig mocken, erzeugen eine Coverage-Zahl, die sich doppelt liest und ein Loch ist — der Defekt lebt genau dazwischen, und beide bleiben sein ganzes Leben grün. Wo dieses Muster existiert, verdrahtet ein Test die echten Dinge, und er wird zuerst rot geschrieben.

Ein Test, der die Maschine misst, ist die andere: zu behaupten, nach sechzig Millisekunden Warten seien zwei Ticks passiert, ist eine Aussage über die Wanduhr, nicht über Verhalten — und sie scheitert unter Last, während sie allein besteht. Warte auf die Bedingung, nicht auf eine Dauer.

Und eine grüne Suite ist nur die halbe Wahrheit. Ein Test-Runner transpiliert ohne Typprüfung, ein Typfehler in einer Testdatei besteht also den Lauf und kippt den Build — deshalb ist der Typecheck ein eigener Schritt über die ganze App, Testdateien eingeschlossen.

Die Schichten und was jede abdeckt.

Test-Schichten und was jede abdeckt
SchichtDeckt ab
Handler-Unit-TestsQueries, Mutations und Actions direkt mit einem Context aufgerufen — kein HTTP, kein Server-Boot.
In-Memory-StoreDie echte DataStore-Schnittstelle: echter Query-Builder, echtes Mixin-Verhalten, echte Change-Events.
Workflow-RunnerDurable Läufe echt ausgeführt — Journal, Signale mitten im Lauf, Suspend und Resume, Replay.
Dialekt-ParitätHandgeschriebenes SQL und Migrationspläne gegen jeden Dialekt; Konvergenz geprüft, nicht angenommen.
voltro e2eFährt api + web hoch, wartet auf Readiness, fährt deine Specs, baut beides ab — auch im Fehlerfall.
voltro typecheckDie andere Hälfte von grün: tsc über die App inklusive Testdateien, was ein Testlauf nicht tut.

Häufige Fragen

Brauche ich eine laufende Datenbank zum Testen?

Für die schnelle Schicht nicht. Der In-Memory-Store implementiert dieselbe Schnittstelle wie die SQL-Stores, Handler-Tests fahren also echtes Verhalten ohne Server. Eine echte Datenbank brauchen Dialekt-Parität und Integrationssuites, die laut überspringen, wenn keine erreichbar ist.

Wie teste ich einen Workflow?

Indem du ihn auf einem echten Runner laufen lässt. Schritte führen aus, das Journal wird geschrieben, und du kannst mitten im Lauf ein Signal zustellen oder prüfen, dass ein Replay fortsetzt statt neu zu starten — der Pfad, an dem durabler Code wirklich bricht.

Was macht voltro e2e?

Fährt api und Web-App hoch, wartet auf Readiness, fährt deine Specs gegen das laufende Paar und baut beides ab, auch im Fehlerfall. Du testest das, was du deployst, statt einer in einer Config-Datei zusammengesetzten Näherung.

Reicht eine grüne Test-Suite zum Mergen?

Nein — ein Runner transpiliert ohne Typprüfung, ein Typfehler in einer Testdatei besteht also die Suite und kippt CI. Fahr den Typecheck über die App inklusive Testdateien; das ist die Hälfte von grün, die leicht und teuer zu überspringen ist.

Kommen Templates mit Tests?

Ja, und sie sind gegated: ein Harness scaffoldet jedes Template in einen echten Workspace, installiert es und fährt seine Suite, mit einem separaten Typecheck-Durchgang. Dieses Gate hat nicht bootende Templates und nicht mehr existierende APIs gefunden, bevor sie jemand gescaffoldet hat.

Was du am Ende testest.

Öffne das Framework. Schau es dir selbst an.

Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.