Pruebe su Worker localmente

CloudflareBeginner
Practicar Ahora

Introducción

Una API de soporte acepta solicitudes válidas, pero falla cuando recibe JSON mal formado. Convertirá ese informe en una prueba que falle, reparará el límite de análisis y ampliará la suite para conservar la validación y el manejo de errores del upstream.

Esta VM independiente proporciona una API pequeña basada en los conceptos de enrutamiento de Build a Support Request API, con una regresión intencional. Node.js 22.22.0, Wrangler 4.131.1 y Miniflare 4.20260730.0 ya están preparados en /home/labex/project/worker-tests. Usted escribirá las pruebas mediante el ejecutor integrado de Node y ejecutará el controlador en workerd a través de Miniflare. Se requieren conocimientos básicos de JavaScript y haber completado la lección anterior sobre la API; aquí se explican las aserciones de pruebas, los hooks y el aislamiento de fixtures.

Este laboratorio es ejecutable y funciona únicamente de forma local. No necesita autorizar ninguna cuenta de Cloudflare ni utilizar recursos remotos o una VM anterior. Todas las solicitudes salientes del Worker son interceptadas por un fixture local, y los entornos de prueba se eliminan después de cada caso.

Escriba pruebas contra el entorno de ejecución de Workers

En este paso, creará una pequeña suite de pruebas para la API de soporte proporcionada. Las pruebas se ejecutan en el ejecutor de pruebas de Node, pero las solicitudes se procesan dentro del entorno workerd de Miniflare, en lugar de importar directamente el controlador en Node.

Entre en el proyecto independiente e inspeccione el controlador proporcionado y las dependencias fijadas:

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

Debería ver Node v22.22.0, Wrangler 4.131.1 y una dependencia directa de Miniflare 4.20260730.0. Estas herramientas ya están instaladas. En su propia máquina, añada la dependencia exacta para las pruebas con npm install --save-dev miniflare@4.20260730.0; si ya existe un archivo de bloqueo, use npm ci. Este laboratorio fija deliberadamente la API 4.x y su fecha de compatibilidad admitida, en lugar de depender de una etiqueta latest que puede cambiar.

Cree test/support.test.mjs. El heredoc entre comillas escribe el módulo literalmente. test declara un caso, assert.equal comprueba un valor escalar y assert.deepEqual compara JSON estructurado. Cada caso asíncrono espera la respuesta antes de realizar las aserciones.

beforeEach inicia un entorno nuevo y restablece la lista de llamadas; afterEach elimina el entorno incluso si una prueba falla. dispatchFetch envía una solicitud de prueba dentro del proceso. Su nombre de host no representa un Worker desplegado. outboundService intercepta cada llamada fetch del Worker y devuelve una respuesta de fixture local; nunca reenvía tráfico a Internet. Comprueba la URL y el método del upstream previstos, y registra la solicitud normalizada. cf: false desactiva la obtención de metadatos de muestra de solicitudes de Cloudflare. No se utiliza ningún inicio de sesión, binding remoto ni despliegue en la nube.

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

Ejecute el comando estándar de pruebas de Node; --test-reporter=spec muestra nombres de casos y totales fáciles de leer:

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

Debería obtener dos pruebas aprobadas y cero fallos. La ruta de health no debe llamar al upstream. La solicitud válida debe producir demo-1 y enviar exactamente una vez el asunto sin espacios iniciales ni finales. Estas pruebas iniciales todavía no cubren el JSON mal formado. Use la verificación para comprobar la suite y el comportamiento independiente del entorno de ejecución.

Consulte la API de Miniflare y la opción de servicio saliente para conocer las interfaces subyacentes. El comportamiento de red en producción todavía requiere pruebas de despliegue independientes.

Añada una prueba de regresión que falle

En este paso, registrará un defecto informado: el JSON mal formado debería producir una respuesta 400 predecible, pero el controlador inicial permite que escape una excepción de análisis.

Añada una prueba con >>; así conservará las dos pruebas existentes. El cuerpo contiene el texto JSON incompleto {. La aserción comprueba el estado de la respuesta antes de analizar el JSON, de modo que el fallo identifique claramente el contrato 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

Debería ver tres pruebas: dos aprobadas y la prueba de JSON mal formado fallida. La aserción debe mostrar que el valor real es 500 y el esperado es 400, y el proceso debe terminar con un código distinto de cero. El entorno de ejecución también puede mostrar la excepción de análisis subyacente. Este es el defecto previsto; no cambie el estado esperado a 500. Un error de importación, un paquete inexistente o el fallo de un caso existente indican un problema diferente.

Inspeccione el await request.json() sin protección del controlador. Ninguna solicitud debe llegar al fixture cuando el JSON es inválido. Use la verificación mientras el defecto siga presente: este paso comprueba específicamente que la prueba de regresión falle y que los casos existentes pasen. En el siguiente paso reparará la implementación.

Repare el análisis de JSON sin debilitar la prueba

En este paso, contendrá únicamente el fallo del análisis de JSON y conservará el enrutamiento, la validación y el manejo del upstream existentes. Reemplace el controlador con esta versión corregida completa. El try/catch alrededor de request.json() convierte una excepción de sintaxis en una respuesta JSON con HTTP 400. El try/catch independiente del upstream continúa gestionando los fallos de red o de respuesta.

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

Las tres pruebas deberían pasar, incluida la prueba de JSON mal formado, que no ha cambiado. Ejecute de nuevo el mismo comando para confirmar que un proceso de pruebas nuevo también pasa:

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

Las dos ejecuciones deberían mostrar tres pruebas aprobadas y cero fallos. Cada caso recibe un entorno nuevo y una lista de llamadas al fixture vacía. No desactive la prueba fallida ni acepte una respuesta 500 para que la suite aparezca en verde. Use la verificación: comprueba tanto las pruebas que usted escribió como un conjunto independiente de respuestas del entorno de ejecución.

Amplíe la cobertura de límites y finalice localmente

En este paso, protegerá el código frente a otras dos regresiones: que una entrada inválida llegue a la dependencia y que una interrupción del upstream aparezca como una solicitud correcta. Añada estos casos sin eliminar los tres anteriores.

La primera prueba recorre valores JSON inválidos y comprueba que devuelvan 422; después comprueba que una entrada de texto no admitida devuelva 415. Ninguna debe llamar al upstream. La segunda comienza con un fixture vacío, simula una respuesta 503 de la dependencia y espera el JSON 502 controlado por la API. Comparar el cuerpo completo de la respuesta también evita que se filtre el diagnóstico interno del 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

Debería obtener cinco pruebas aprobadas y cero fallos. Ejecute la suite de nuevo; demo-1 y una única llamada registrada por la interrupción deben mantenerse estables porque el estado del fixture se restablece en cada caso.

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

Todo el trabajo se mantuvo local: las URL de prueba se enviaron a Miniflare, cada llamada saliente del Worker fue interceptada y cada entorno se eliminó. Confirme que la VM no tiene ninguna sesión de Cloudflare almacenada:

npx wrangler whoami --json

Debería aparecer "loggedIn": false; el comando de estado sin autenticación puede terminar con un código distinto de cero. No inicie sesión para este laboratorio. No hay recursos en la nube que eliminar. Use la verificación final, que comprueba la API local real y confirma que sus pruebas rechazan copias defectuosas desechables y aceptan el código reparado. Estas copias de evaluación no modifican su proyecto. Después, finalice la VM.

Las pruebas del entorno de ejecución local hacen que las regresiones sean reproducibles. No verifican la propiedad de la cuenta, la configuración del despliegue, las dependencias reales de Internet ni el comportamiento del despliegue en los nodos perimetrales; para ello se necesitan las comprobaciones remotas del curso.

Resumen

Escribió pruebas que ejecutan la API en un entorno de ejecución local de Workers, reprodujo una regresión de 500 frente a 400 y reparó la implementación sin debilitar el contrato esperado. Añadió casos para la entrada y los errores del upstream, restableció el estado del fixture entre casos y eliminó cada entorno. La suite se mantuvo local y repetible, sin credenciales de nube ni escrituras de recursos.