Worker lokal testen

CloudflareBeginner
Jetzt üben

Einführung

Eine Support-API akzeptiert gültige Anfragen, stürzt aber bei fehlerhaftem JSON ab. Sie wandeln diese Fehlermeldung in einen fehlschlagenden Test um, reparieren die Parsing-Grenze und erweitern die Testsuite, damit Validierung und Fehlerbehandlung für Upstream-Aufrufe erhalten bleiben.

Diese unabhängige VM stellt eine kleine API bereit, die auf den Routing-Konzepten aus „Eine Support-Request-API erstellen“ basiert und eine absichtlich eingebaute Regression enthält. Node.js 22.22.0, Wrangler 4.131.1 und Miniflare 4.20260730.0 sind unter /home/labex/project/worker-tests vorbereitet. Sie schreiben die Tests selbst mit dem integrierten Node-Test-Runner und führen den Handler über Miniflare in der workerd-Laufzeit aus. Grundkenntnisse in JavaScript und die vorherige API-Lektion werden vorausgesetzt; Test-Assertions, Hooks und die Isolation von Test-Fixtures werden hier erklärt.

Dies ist ein ausführbares Labor, das vollständig lokal arbeitet. Sie benötigen keine Autorisierung für ein Cloudflare-Konto, keine entfernte Ressource und keine vorherige VM. Alle ausgehenden Worker-Anfragen werden von einer lokalen Fixture abgefangen, und die Testlaufzeiten werden nach jedem Testfall freigegeben.

Tests für die Workers-Laufzeit schreiben

In diesem Schritt erstellen Sie eine kleine Testsuite für die bereitgestellte Support-API. Die Tests laufen im Node-Test-Runner, die Anfragen werden jedoch innerhalb der Miniflare-workerd-Laufzeit ausgeführt, statt den Handler direkt in Node zu importieren.

Wechseln Sie in das unabhängige Projekt und prüfen Sie den bereitgestellten Handler sowie die festgelegten Abhängigkeiten:

cd /home/labex/project/worker-tests
node --version
npx wrangler --version
npm ls miniflare --depth=0
cat src/index.js

Erwartet werden Node v22.22.0, Wrangler 4.131.1 und eine direkte Miniflare-Abhängigkeit in der Version 4.20260730.0. Diese Werkzeuge sind bereits installiert. Installieren Sie auf Ihrem eigenen Rechner die exakte Testabhängigkeit mit npm install --save-dev miniflare@4.20260730.0; verwenden Sie bei einer vorhandenen Lockfile npm ci. Dieses Labor legt die 4.x-API und das unterstützte Kompatibilitätsdatum bewusst fest, statt sich auf ein sich ständig änderndes latest-Tag zu verlassen.

Erstellen Sie test/support.test.mjs. Das angegebene Here-Dokument schreibt das Modul unverändert. test definiert einen Testfall, assert.equal prüft einen einzelnen Wert und assert.deepEqual vergleicht strukturiertes JSON. Jeder asynchrone Testfall wartet auf seine Antwort, bevor er Assertions ausführt.

beforeEach startet eine neue Laufzeit und setzt die Aufrufliste zurück; afterEach gibt die Laufzeit auch dann frei, wenn ein Test fehlschlägt. dispatchFetch sendet eine In-Process-Testanfrage. Der Hostname steht nicht für einen bereitgestellten Worker. outboundService fängt jeden Fetch des Workers ab und gibt eine lokale Fixture-Antwort zurück; Datenverkehr ins Internet wird niemals weitergeleitet. Die Funktion prüft die vorgesehene Upstream-URL und -Methode und zeichnet die normalisierte Anfrage auf. cf: false deaktiviert das Abrufen beispielhafter Cloudflare-Anfragemetadaten. Es werden weder Anmeldung noch Remote-Binding oder Cloud-Bereitstellung verwendet.

cat > test/support.test.mjs <<'JS'
import {test, beforeEach, afterEach} from 'node:test';
import assert from 'node:assert/strict';
import {fileURLToPath} from 'node:url';
import {Miniflare} from 'miniflare';

let mf;
let calls;
beforeEach(() => {
  calls = [];
  mf = new Miniflare({
    modules: true,
    scriptPath: fileURLToPath(new URL('../src/index.js', import.meta.url)),
    compatibilityDate: '2026-07-30',
    cf: false,
    bindings: {UPSTREAM_URL: 'https://tickets.test'},
    outboundService: async (request) => {
      assert.equal(request.url, 'https://tickets.test/tickets');
      assert.equal(request.method, 'POST');
      const body = await request.json();
      calls.push(body);
      if (body.subject === 'simulate-outage') {
        return new Response('SIMULATED_INTERNAL_DETAIL', {status: 503});
      }
      return Response.json({ticket: `demo-${calls.length}`, subject: body.subject}, {status: 201});
    }
  });
});
afterEach(async () => { await mf.dispose(); });

test('health stays public', async () => {
  const response = await mf.dispatchFetch('http://worker.test/health');
  assert.equal(response.status, 200);
  assert.deepEqual(await response.json(), {status: 'ok'});
  assert.equal(calls.length, 0);
});

test('valid request reaches the local fixture', async () => {
  const response = await mf.dispatchFetch('http://worker.test/requests', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({subject: '  Printer offline  '})
  });
  assert.equal(response.status, 201);
  assert.deepEqual(await response.json(), {ticket: 'demo-1', subject: 'Printer offline'});
  assert.deepEqual(calls, [{subject: 'Printer offline'}]);
});
JS

Führen Sie den standardmäßigen Node-Testbefehl aus; --test-reporter=spec gibt gut lesbare Testnamen und Summen aus:

node --test --test-reporter=spec test/support.test.mjs

Erwartet werden zwei erfolgreiche Tests und keine Fehler. Der Health-Endpunkt darf den Upstream nicht aufrufen. Die gültige Anfrage muss demo-1 erzeugen und genau einmal ein bereinigtes Subject senden. Diese ersten Tests decken fehlerhaftes JSON noch nicht ab. Verwenden Sie die Verifikation, um die Testsuite und das unabhängige Verhalten der Laufzeit zu prüfen.

Weitere Informationen zu den zugrunde liegenden Schnittstellen finden Sie in der Miniflare API und der Option für den Outbound-Service. Das Netzwerkverhalten in der Produktion muss weiterhin separat durch eine Bereitstellung getestet werden.

Einen fehlschlagenden Regressionstest hinzufügen

In diesem Schritt halten Sie einen gemeldeten Fehler fest: Fehlerhaftes JSON soll eine vorhersehbare 400-Antwort erzeugen, aber der Starter-Handler lässt derzeit eine Parsing-Ausnahme entweichen.

Hängen Sie mit >> einen Test an. Dadurch bleiben die beiden vorhandenen Tests erhalten. Der Request-Body enthält den unvollständigen JSON-Text {. Die Assertion prüft den Antwortstatus, bevor sie JSON parst, damit der Fehler eindeutig den HTTP-Vertrag kennzeichnet.

cat >> test/support.test.mjs <<'JS'

test('malformed JSON returns 400 before the upstream', async () => {
  const response = await mf.dispatchFetch('http://worker.test/requests', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: '{'
  });
  assert.equal(response.status, 400);
  assert.deepEqual(await response.json(), {error: 'invalid_json'});
  assert.equal(calls.length, 0);
});
JS
node --test --test-reporter=spec test/support.test.mjs

Erwartet werden drei Tests: Zwei bestehen, der Test für fehlerhaftes JSON schlägt fehl. Die Assertion meldet tatsächlich 500 statt der erwarteten 400, und der Prozess wird mit einem Fehlerstatus beendet. Die Laufzeit kann zusätzlich die zugrunde liegende Parsing-Ausnahme ausgeben. Das ist der beabsichtigte Fehler und kein Grund, den erwarteten Status auf 500 zu ändern. Ein Importfehler, ein fehlendes Paket oder das Fehlschlagen eines bestehenden Testfalls weist auf ein anderes Problem hin.

Prüfen Sie, ob der Handler await request.json() ungeschützt aufruft. Bei ungültigem JSON darf keine Anfrage die Fixture erreichen. Verwenden Sie die Verifikation, solange der Fehler noch vorhanden ist: Dieser Schritt prüft ausdrücklich, dass der Regressionstest fehlschlägt und die vorhandenen Testfälle bestehen. Im nächsten Schritt reparieren Sie die Implementierung.

JSON-Parsing reparieren, ohne den Test abzuschwächen

In diesem Schritt fangen Sie ausschließlich den Fehler beim JSON-Parsing ab und erhalten das vorhandene Routing, die Validierung und die Upstream-Fehlerbehandlung. Ersetzen Sie den Handler durch diese vollständige korrigierte Version. Der try/catch-Block um request.json() wandelt eine Syntaxausnahme in eine JSON-Antwort mit HTTP 400 um. Der separate try/catch-Block für den Upstream behandelt weiterhin Netzwerk- oder Antwortfehler.

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    try {
      const upstream = await fetch(`${env.UPSTREAM_URL}/tickets`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({subject})
      });
      if (!upstream.ok) {
        return Response.json({error: 'upstream_unavailable'}, {status: 502});
      }
      const ticket = await upstream.json();
      return Response.json({ticket: ticket.ticket, subject}, {status: 201});
    } catch {
      return Response.json({error: 'upstream_unavailable'}, {status: 502});
    }
  }
};
JS
node --test --test-reporter=spec test/support.test.mjs

Erwartet werden drei erfolgreiche Tests, einschließlich des unveränderten Tests für fehlerhaftes JSON. Führen Sie denselben Befehl erneut aus, um zu bestätigen, dass auch ein neuer Testprozess erfolgreich ist:

node --test --test-reporter=spec test/support.test.mjs

Beide Durchläufe sollten drei erfolgreiche Tests und keine Fehler melden. Jeder Testfall erhält eine neue Laufzeit und eine leere Fixture-Aufrufliste. Deaktivieren Sie den fehlschlagenden Test nicht und akzeptieren Sie keine 500-Antwort, nur damit die Testsuite grün wird. Verwenden Sie die Verifikation: Sie prüft sowohl Ihre Tests als auch eine separate Gruppe von Laufzeitantworten.

Grenzfälle abdecken und lokal abschließen

In diesem Schritt schützen Sie sich gegen zwei weitere Regressionen: Ungültige Eingaben dürfen die Abhängigkeit nicht erreichen, und ein Ausfall des Upstreams darf nicht als erfolgreiche Anfrage erscheinen. Hängen Sie diese Testfälle an, ohne die bisherigen drei zu entfernen.

Der erste Test durchläuft mehrere ungültige JSON-Werte und prüft jeweils 422. Anschließend testet er eine nicht unterstützte Texteingabe auf 415. Keiner dieser Fälle darf den Upstream aufrufen. Der zweite Test beginnt mit einer leeren Fixture, simuliert eine 503-Antwort der Abhängigkeit und erwartet das abgefangene 502-JSON der API. Der Vergleich des vollständigen Antwort-Bodys verhindert außerdem, dass die interne Diagnose der Fixture nach außen gelangt.

cat >> test/support.test.mjs <<'JS'

test('invalid subjects and media types never reach the upstream', async () => {
  for (const body of [null, [], {}, {subject: 5}, {subject: ' '}, {subject: 'x'.repeat(81)}]) {
    const response = await mf.dispatchFetch('http://worker.test/requests', {
      method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)
    });
    assert.equal(response.status, 422);
    assert.deepEqual(await response.json(), {error: 'invalid_subject'});
  }
  const response = await mf.dispatchFetch('http://worker.test/requests', {
    method: 'POST', headers: {'Content-Type': 'text/plain'}, body: 'hello'
  });
  assert.equal(response.status, 415);
  assert.deepEqual(await response.json(), {error: 'unsupported_media_type'});
  assert.equal(calls.length, 0);
});

test('upstream errors are contained with fresh fixture state', async () => {
  assert.equal(calls.length, 0);
  const response = await mf.dispatchFetch('http://worker.test/requests', {
    method: 'POST', headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({subject: 'simulate-outage'})
  });
  assert.equal(response.status, 502);
  assert.deepEqual(await response.json(), {error: 'upstream_unavailable'});
  assert.deepEqual(calls, [{subject: 'simulate-outage'}]);
});
JS
node --test --test-reporter=spec test/support.test.mjs

Erwartet werden fünf erfolgreiche Tests und keine Fehler. Führen Sie die Testsuite erneut aus. demo-1 und genau ein aufgezeichneter Ausfallaufruf müssen stabil bleiben, weil der Fixture-Zustand für jeden Testfall zurückgesetzt wird.

node --test --test-reporter=spec test/support.test.mjs

Die gesamte Arbeit blieb lokal: Die Test-URLs wurden an Miniflare übergeben, jeder ausgehende Worker-Aufruf wurde abgefangen und jede Laufzeit wurde freigegeben. Prüfen Sie, dass in der VM keine Cloudflare-Anmeldung gespeichert ist:

npx wrangler whoami --json

Erwartet wird "loggedIn": false; der nicht authentifizierte Statusbefehl kann mit einem Fehlerstatus beendet werden. Melden Sie sich für dieses Labor nicht an. Es gibt keine Cloud-Ressourcen, die gelöscht werden müssen. Verwenden Sie die abschließende Verifikation. Sie prüft die echte lokale API und bestätigt, dass Ihre Tests sowohl verworfene fehlerhafte Kopien ablehnen als auch den reparierten Code akzeptieren. Diese Bewertungskopien ändern Ihr Projekt nicht. Beenden Sie anschließend die VM.

Tests in der lokalen Laufzeit machen Regressionen reproduzierbar. Sie prüfen jedoch weder Kontobesitz noch Bereitstellungseinstellungen, echte Internetabhängigkeiten oder das Verhalten beim Ausrollen an Edge-Standorten. Dafür sind die Remote-Prüfungen des Kurses erforderlich.

Zusammenfassung

Sie haben Tests geschrieben, die die API in einer lokalen Workers-Laufzeit ausführen, eine Regression von 500 gegenüber 400 reproduziert und die Implementierung repariert, ohne den erwarteten Vertrag abzuschwächen. Sie haben Fälle für Eingabefehler und Upstream-Fehler ergänzt, den Fixture-Zustand zwischen den Testfällen zurückgesetzt und jede Laufzeit freigegeben. Die Testsuite blieb ohne Cloud-Anmeldedaten oder Änderungen an Ressourcen lokal und wiederholbar.