Tester votre Worker localement

CloudflareBeginner
Pratiquer maintenant

Introduction

Une API de support accepte les requêtes valides, mais s’arrête lorsqu’elle reçoit un JSON mal formé. Vous allez transformer ce signalement en test qui échoue, corriger la limite de parsing, puis étendre la suite afin de préserver la validation et la gestion des erreurs en amont.

Cette VM indépendante fournit une petite API basée sur les concepts de routage de l’atelier « Créer une API de requêtes de support », avec une régression introduite volontairement. Node.js 22.22.0, Wrangler 4.131.1 et Miniflare 4.20260730.0 sont préinstallés dans /home/labex/project/worker-tests. Vous écrirez vous-même les tests avec le runner intégré de Node et exécuterez le gestionnaire dans le runtime workerd via Miniflare. Des connaissances de base en JavaScript et l’atelier précédent sur l’API sont nécessaires ; les assertions de test, les hooks et l’isolation des fixtures sont expliqués ici.

Il s’agit d’un atelier exécutable uniquement en local. Aucun compte Cloudflare, aucune ressource distante ni aucune VM précédente ne sont nécessaires. Toutes les requêtes sortantes du Worker sont interceptées par une fixture locale, et les runtimes de test sont libérés après chaque cas.

Écrire des tests avec le runtime Workers

Dans cette étape, vous allez créer une petite suite de tests autour de l’API de support fournie. Les tests s’exécutent avec le runner de tests de Node, mais les requêtes sont traitées dans le runtime workerd de Miniflare au lieu d’importer directement le gestionnaire dans Node.

Accédez au projet indépendant et examinez le gestionnaire ainsi que les dépendances verrouillées :

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

Vous devez obtenir Node v22.22.0, Wrangler 4.131.1 et une dépendance directe vers Miniflare 4.20260730.0. Ces outils sont préinstallés. Sur votre propre machine, ajoutez exactement la dépendance de test avec npm install --save-dev miniflare@4.20260730.0 ; utilisez npm ci si un fichier lock existe déjà. Cet atelier fixe volontairement l’API 4.x et sa date de compatibilité prise en charge, au lieu de dépendre d’un tag latest susceptible d’évoluer.

Créez test/support.test.mjs. Le heredoc entre apostrophes écrit littéralement le module. test déclare un cas, assert.equal vérifie une valeur scalaire et assert.deepEqual compare des objets JSON structurés. Chaque cas asynchrone attend la réponse avant d’effectuer les assertions.

beforeEach démarre un nouveau runtime et réinitialise la liste des appels ; afterEach libère le runtime même si un test échoue. dispatchFetch envoie une requête de test en mémoire. Son nom d’hôte ne correspond pas à un Worker déployé. outboundService intercepte chaque appel fetch du Worker et renvoie une réponse issue d’une fixture locale ; il ne transmet jamais de trafic à Internet. Il vérifie l’URL et la méthode attendues pour le service en amont et enregistre la requête normalisée. cf: false désactive la récupération d’exemples de métadonnées de requête Cloudflare. Aucune connexion, liaison distante ni aucun déploiement cloud n’est utilisé.

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

Exécutez la commande de test standard de Node ; --test-reporter=spec affiche des noms de cas et des totaux faciles à lire :

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

Vous devez obtenir deux tests réussis et aucune erreur. La route de santé ne doit pas appeler le service en amont. La requête valide doit produire demo-1 et envoyer exactement une fois le sujet sans espaces superflus. Ces premiers tests ne couvrent pas encore le JSON mal formé. Utilisez la vérification pour contrôler la suite et le comportement indépendant du runtime.

Consultez la documentation de l’API Miniflare et l’option outbound service pour connaître les interfaces sous-jacentes. Le comportement réseau en production doit être testé séparément après le déploiement.

Ajouter un test de régression qui échoue

Dans cette étape, vous allez reproduire un défaut signalé : un JSON mal formé doit produire une réponse 400 prévisible, mais le gestionnaire initial laisse échapper l’exception de parsing.

Ajoutez un test avec >>, ce qui conserve les deux tests existants. Le corps est le texte JSON incomplet {. L’assertion vérifie le statut de la réponse avant de parser le JSON afin que l’échec identifie clairement le contrat HTTP.

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

Vous devez obtenir trois tests : deux réussissent et le test du JSON mal formé échoue. L’assertion indique 500 comme valeur réelle au lieu de 400 comme valeur attendue, et le processus se termine avec un code différent de zéro. Le runtime peut également afficher l’exception de parsing sous-jacente. Il s’agit du défaut attendu ; ne modifiez pas le statut attendu pour accepter 500. Une erreur d’importation, un package manquant ou l’échec d’un cas existant correspond à un autre problème.

Examinez l’appel non protégé à await request.json(). Aucune requête ne doit atteindre la fixture lorsque le JSON est invalide. Utilisez la vérification tant que le défaut est présent : cette étape vérifie précisément que le test de régression échoue et que les cas existants réussissent. L’étape suivante corrigera l’implémentation.

Corriger le parsing JSON sans affaiblir le test

Dans cette étape, vous allez intercepter uniquement l’échec du parsing JSON tout en conservant le routage, la validation et la gestion existants des erreurs en amont. Remplacez le gestionnaire par cette version complète corrigée. Le try/catch autour de request.json() transforme une exception de syntaxe en réponse JSON avec le statut HTTP 400. Le try/catch distinct autour de l’appel en amont continue de gérer les erreurs réseau ou les réponses en échec.

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

Les trois tests doivent réussir, y compris le test inchangé du JSON mal formé. Exécutez de nouveau la même commande pour confirmer qu’un nouveau processus de test réussit également :

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

Les deux exécutions doivent indiquer trois réussites et aucune erreur. Chaque cas reçoit un nouveau runtime et une liste d’appels de fixture vide. Ne désactivez pas le test qui échoue et n’acceptez pas une réponse 500 pour faire passer la suite au vert. Utilisez la vérification : elle contrôle à la fois les tests écrits par l’apprenant et un ensemble séparé de réponses du runtime.

Étendre la couverture des limites et terminer localement

Dans cette étape, vous allez vous protéger contre deux autres régressions : une entrée invalide qui atteindrait la dépendance et une panne du service en amont qui apparaîtrait comme une requête réussie. Ajoutez ces cas sans supprimer les trois précédents.

Le premier test parcourt plusieurs valeurs JSON invalides et vérifie le statut 422, puis contrôle qu’une entrée texte non prise en charge produit 415. Aucun de ces cas ne doit appeler le service en amont. Le second commence avec une fixture vide, simule une réponse 503 de la dépendance et attend le JSON 502 contenu par l’API. La comparaison du corps de réponse complet empêche également la fuite du diagnostic interne de la fixture.

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

Vous devez obtenir cinq réussites et aucune erreur. Exécutez de nouveau la suite ; demo-1 et un seul appel de panne enregistré doivent rester stables, car l’état de la fixture est réinitialisé pour chaque cas.

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

Tout le travail est resté local : les URL de test ont été envoyées à Miniflare, chaque appel sortant du Worker a été intercepté et chaque runtime a été libéré. Vérifiez que la VM ne contient aucune connexion Cloudflare enregistrée :

npx wrangler whoami --json

Vous devez obtenir "loggedIn": false ; la commande d’état non authentifiée peut se terminer avec un code différent de zéro. Ne vous connectez pas pour cet atelier. Aucune ressource cloud ne doit être supprimée. Utilisez la vérification finale : elle contrôle l’API locale réelle et confirme que vos tests rejettent des copies défectueuses temporaires tout en acceptant le code corrigé. Ces copies d’évaluation ne modifient pas votre projet. Terminez ensuite la VM.

Les tests du runtime local rendent les régressions reproductibles. Ils ne vérifient ni la propriété du compte, ni les paramètres de déploiement, ni les dépendances Internet réelles, ni le comportement du déploiement en périphérie ; ces éléments nécessitent les vérifications distantes du cours.

Résumé

Vous avez écrit des tests qui exécutent l’API dans un runtime Workers local, reproduit une régression 500 contre 400 et corrigé l’implémentation sans affaiblir le contrat attendu. Vous avez ajouté des cas pour les entrées invalides et les erreurs en amont, réinitialisé l’état de la fixture entre les cas et libéré chaque runtime. La suite est restée locale et reproductible, sans identifiants cloud ni écriture de ressources.