Cloudflare Workers
Testing
Vitest
Miniflare
CI/CD

Testing Workers: Einheit, Integration und wie man die Laufzeit simuliert, ohne von Cloudflare abhängig zu sein

Die Workers-Laufzeit ist nicht Node.js – Tests ohne Berücksichtigung dieser Tatsache stellen sicher, dass Tests lokal erfolgreich sind und Fehler in der Produktion auftreten.

Testing Workers: Einheit, Integration und wie man die Laufzeit simuliert, ohne von Cloudflare abhängig zu sein

Das Testen von Workern mit Jest auf Node.js ist eine elegante Falle. Die Tests werden bestanden, das CI wird grün und der Code kommt mit Fehlern in der Produktion an, die die Testumgebung niemals erkennen konnte – weil die Testumgebung nicht die Laufzeit ist, die den Code in der Produktion ausführt. Worker laufen auf V8-Isolaten mit den Webplattform-APIs, nicht mit den Node.js-APIs: Es gibt kein Buffer, es gibt kein process.env, das fetch ist die native Implementierung der Laufzeit (weder das node-fetch noch das undici) und crypto ist die Web-Crypto-API mit ihren asynchronen Signaturen. Ein fetch-Mock, der für Jest in Node.js geschrieben wurde, besteht möglicherweise die Tests und funktioniert stillschweigend nicht, wenn der Worker mit den echten Response vom Rand aus zu tun hat.

Warum die Testumgebung wichtiger ist, als es scheint

Der Unterschied zwischen Node.js und der Workers-Laufzeit geht über die verfügbaren APIs hinaus: Edge-Verhalten tritt nur unter bestimmten Bedingungen auf. Die TextEncoder in Node.js und die TextEncoder in der Workers-Laufzeit haben dieselbe Schnittstelle, aber die von ihnen erzeugten Uint8Array verhalten sich je nach Version und Implementierung möglicherweise unterschiedlich, wenn sie an eine SubtleCrypto-Operation übergeben werden. Workers Headers weist ein Verhalten bei der Normalisierung von Headernamen auf (Groß-/Kleinschreibung wird nicht beachtet, lexikografische Reihenfolge), das sich von Scheinimplementierungen unterscheidet.

Am wichtigsten: Bindungen gibt es in Node.js nicht. env.KV, env.DB, env.BUCKET sind Objekte, die von der Laufzeit eingefügt werden – es gibt keine Möglichkeit, ein npm-Paket zu importieren, das ihnen volle Wiedergabetreue bietet. Manuelle Mocks testen den Code, der den Mock aufruft, nicht den Code, der mit der tatsächlichen Bindung funktioniert.

@cloudflare/vitest-pool-workers löst dieses Problem, indem die Tests in einer echten Workers-Laufzeit ausgeführt werden – insbesondere Miniflare v3, das ein echtes V8-Isolat ausführt, keine Node.js-Simulation. Tests haben Zugriff auf caches, crypto, fetch, Headers, ReadableStream, TransformStream mit Laufzeitimplementierungen. Bindungen werden als In-Process-Implementierungen bereitgestellt: KV im Speicher, D1 als eingebettetes SQLite, R2 als lokaler Speicher. Das Ergebnis ist, dass ein Test, der mit vitest-pool-workers besteht, Code testet, der in der realen Laufzeit funktioniert.

Konfigurieren von vitest.config.ts mit Bindungen

In der Workers-Pool-Konfiguration in vitest.config.ts deklarieren Sie die Bindungen, die die Tests erhalten. Die Struktur spiegelt wrangler.toml wider, jedoch in der Vitest-Konfiguration:

import { defineConfig } from 'vitest/config'; import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' }, miniflare: { kvNamespaces: ['CACHE'], d1Databases: ['DB'], r2Buckets: ['BUCKET'], bindings: { APP_ENV: 'test', }, }, }, }, }, });

Mit dieser Konfiguration erhält jede Testsuite eine saubere Umgebung mit einem leeren KV-Namespace, einer leeren D1-SQLite-Datenbank (mit dem angewendeten Schema, wenn Sie migrations konfigurieren) und einem leeren R2-Bucket. Bindungen sind zwischen den Suiten isoliert – Änderungen, die durch Tests in einer Suite vorgenommen werden, werden nicht an eine andere weitergegeben.

Um in Tests auf die Bindungen zuzugreifen, exportiert @cloudflare/vitest-pool-workers einen Helfer env, den Miniflare über einen globalen Mechanismus einfügt:

import { env } from 'cloudflare:test'; test('deve retornar 404 para usuário inexistente', async () => { const request = new Request('https://api.example.com/users/999'); const response = await SELF.fetch(request); expect(response.status).toBe(404); });

SELF ist die Bindung des zu testenden Workers selbst – Sie können SELF.fetch() ausführen, um den Fetch-Handler als integrierten HTTP-Aufruf mit vollständigem Routing und allen verfügbaren Bindungen zu testen.

Die Trennung zwischen Unit- und Integrationstests

Geschäftslogikfunktionen, die mit reinen JavaScript-Werten arbeiten – ein JWT analysieren, einen reduzierten Preis berechnen, ein Eingabeschema validieren – können und sollten ohne den Workers-Pool getestet werden. Diese Funktionen sind nicht von Laufzeit-APIs oder Bindungen abhängig und das Testen in reinem Node.js mit Standard-Vitest ist schneller und einfacher. Als Faustregel gilt: Wenn die Funktion JavaScript-Primitivwerte oder flache Objekte annimmt und zurückgibt, gehört sie zum Node.js-Pool. Wenn es Request, Response, Headers, ReadableStream oder eine beliebige Bindung verwendet, gehört es zum Workers-Pool.

Integrations-HTTP-Handler – die fetch(request, env, ctx)-Funktion, die den Worker-Einstiegspunkt darstellt – benötigen, dass der Worker-Pool zuverlässig getestet wird. Um einen Integrationshandler zu testen, müssen Sie Schritt für Schritt vorgehen und den Status, die Antwortheader und den Text der Antwort überprüfen, die der Worker in der Produktion erzeugen würde. Dieser Test hat auch einen Nebeneffekt auf Bindungen: Wenn der Handler einen Datensatz in D1 speichern soll, kann der Test später env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first() ausführen und überprüfen, ob die Daten korrekt geschrieben wurden.

CI ohne Cloudflare-Anmeldeinformationen – und die Grenzen von Miniflare

vitest-pool-workers mit Miniflare läuft vollständig lokal – führt keine Aufrufe an die Cloudflare-API durch und benötigt weder CLOUDFLARE_ACCOUNT_ID noch CLOUDFLARE_API_TOKEN im CI. Miniflare v3 lädt workerd (die Open-Source-Laufzeitumgebung von Cloudflare) als lokale Binärdatei herunter und führt es wie jeden anderen Prozess aus. Die CI-Pipeline läuft auf npm ci und npx vitest run hinaus – keine Geheimnisse, keine Netzwerkabhängigkeit außer dem Binär-Download bei der ersten Installation.

Miniflare v3 simuliert die Laufzeit mit hoher Genauigkeit, es gibt jedoch dokumentierte Ausnahmen, die für die Teststrategie von Bedeutung sind. Langlebige Objekte im Produktionsmodus – mit ihrer globalen Singleton-Garantie – werden nicht mit verteilter Konsistenz simuliert; Miniflare erstellt eine lokale Instanz, die für die grundlegende Logik nützlich ist, testet jedoch nicht das tatsächliche Koordinationsverhalten. CDN-Verhaltensweisen wie Edge-Caching und das cf-Objekt mit echten Geolocation-Daten müssen wrangler dev --remote ausgeübt werden.

In diesen Fällen sind Staging-Smoke-Tests die Antwort, die nach der Bereitstellung direkte HTTP-Aufrufe an einen Staging-Worker tätigen. Sie ersetzen Miniflare-Tests nicht – sie decken das ab, was Miniflare nicht simulieren kann: Verhaltensweisen, die nur auftreten, wenn Code auf einer echten Infrastruktur ausgeführt wird.

WaitUntil und Hintergrundarbeit testen

Im ctx.waitUntil() erfolgt die Arbeit, nachdem die Antwort gesendet wurde. Der SELF.fetch() in den Tests gibt die Antwort sofort zurück, aber die an waitUntil weitergegebene Arbeit ist möglicherweise noch nicht abgeschlossen. @cloudflare/vitest-pool-workers bietet waitOnExecutionContext, um dieses Problem zu lösen:

import { env, SELF, waitOnExecutionContext } from 'cloudflare:test'; test('deve enfileirar analytics em background', async () => { const ctx = createExecutionContext(); const response = await SELF.fetch(new Request('https://api.example.com/checkout')); await waitOnExecutionContext(ctx); const queued = await env.ANALYTICS_QUEUE.read(); expect(queued).toHaveLength(1); });

Ohne waitOnExecutionContext kommt es bei einem Test, der auf die Nebenwirkung von waitUntil prüft, zu einer Race-Bedingung – einer Flockenbildung, die schwer zu diagnostizieren ist, da das Problem nicht im Produktionscode, sondern im Testcode liegt.

Lesen Sie auch