Teste seu Worker localmente

CloudflareBeginner
Pratique Agora

Introdução

Uma API de suporte aceita requisições válidas, mas falha quando recebe JSON malformado. Você transformará esse relato em um teste que falha, corrigirá o limite de análise do JSON e ampliará a suíte para preservar a validação e o tratamento de erros do upstream.

Esta VM independente fornece uma pequena API baseada nos conceitos de roteamento de Build a Support Request API, com uma regressão intencional. Node.js 22.22.0, Wrangler 4.131.1 e Miniflare 4.20260730.0 já estão preparados em /home/labex/project/worker-tests. Você escreverá os testes usando o runner integrado do Node e executará o handler no runtime workerd por meio do Miniflare. É necessário ter conhecimentos básicos de JavaScript e ter concluído a lição anterior sobre a API; as asserções de teste, os hooks e o isolamento de fixtures são explicados aqui.

Este é um laboratório executável apenas localmente. Não é necessária autorização de uma conta da Cloudflare, nenhum recurso remoto nem uma VM anterior. Todas as requisições de saída do Worker são interceptadas por uma fixture local, e os runtimes de teste são descartados após cada caso.

Escreva testes para o runtime do Workers

Nesta etapa, você criará uma pequena suíte de testes para a API de suporte fornecida. Os testes são executados no runner de testes do Node, mas as requisições são processadas dentro do runtime workerd do Miniflare, em vez de importar o handler diretamente no Node.

Entre no projeto independente e inspecione o handler e as dependências fixadas:

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

Você deve encontrar Node v22.22.0, Wrangler 4.131.1 e uma dependência direta do Miniflare 4.20260730.0. Essas ferramentas já estão instaladas. Na sua própria máquina, adicione a dependência exata de teste com npm install --save-dev miniflare@4.20260730.0; use npm ci quando já existir um lockfile. Este laboratório fixa a API da versão 4.x e a data de compatibilidade compatível de propósito, em vez de depender de uma tag latest que pode mudar.

Crie test/support.test.mjs. O heredoc entre aspas grava o módulo literalmente. test declara um caso, assert.equal verifica um valor simples e assert.deepEqual compara JSON estruturado. Cada caso assíncrono aguarda a resposta antes de fazer as asserções.

beforeEach inicia um novo runtime e redefine a lista de chamadas; afterEach descarta o runtime mesmo quando um teste falha. dispatchFetch envia uma requisição de teste em processo. O nome do host não representa um Worker implantado. outboundService intercepta todas as chamadas feitas pelo Worker e retorna uma resposta de fixture local; nunca encaminha tráfego para a Internet. Ele verifica a URL e o método esperados do upstream e registra a requisição normalizada. cf: false desativa a busca de metadados de requisição de exemplo da Cloudflare. Nenhum login, binding remoto ou deployment na nuvem é usado.

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

Execute o comando padrão de testes do Node; --test-reporter=spec exibe nomes de casos e totais de forma legível:

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

Você deve ver dois testes aprovados e nenhuma falha. O endpoint de health não deve chamar o upstream. A requisição válida deve produzir demo-1 e enviar um subject aparado exatamente uma vez. Esses testes iniciais ainda não cobrem JSON malformado. Use a verificação para conferir a suíte e o comportamento independente do runtime.

Consulte a API do Miniflare e a opção de serviço de saída para conhecer as interfaces subjacentes. O comportamento da rede em produção ainda precisa de testes de deployment separados.

Adicione um teste de regressão que falha

Nesta etapa, você registrará um defeito relatado: JSON malformado deve produzir uma resposta 400 previsível, mas o handler inicial deixa uma exceção de análise escapar.

Adicione um teste usando >>; isso preserva os dois testes existentes. O corpo contém o texto JSON incompleto {. A asserção verifica o status da resposta antes de analisar o JSON, para que a falha identifique claramente o 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

Você deve ver três testes: dois aprovados e o teste de JSON malformado falhando. A asserção deve indicar 500 como valor real e 400 como valor esperado, e o processo deve terminar com status diferente de zero. O runtime também pode exibir a exceção de análise subjacente. Esse é o defeito esperado; não altere o status esperado para 500. Um erro de importação, um pacote ausente ou a falha de um caso existente representa outro problema.

Inspecione o await request.json() sem proteção no handler. Nenhuma requisição deve chegar à fixture quando o JSON é inválido. Use a verificação enquanto o defeito ainda estiver presente: esta etapa confirma especificamente que o teste de regressão falha e que os casos existentes passam. Na próxima etapa, você corrigirá a implementação.

Corrija a análise de JSON sem enfraquecer o teste

Nesta etapa, você conterá apenas a falha de análise do JSON e preservará o roteamento, a validação e o tratamento do upstream existentes. Substitua o handler por esta versão corrigida completa. O try/catch em torno de request.json() transforma uma exceção de sintaxe em JSON com HTTP 400. O try/catch separado do upstream continua tratando falhas de rede ou de resposta.

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

Todos os três testes devem passar, incluindo o teste de JSON malformado, que permanece inalterado. Execute o mesmo comando novamente para confirmar que um novo processo de teste também passa:

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

As duas execuções devem informar três testes aprovados e nenhuma falha. Cada caso recebe um novo runtime e uma lista vazia de chamadas da fixture. Não desative o teste que falhava nem aceite uma resposta 500 para deixar a suíte verde. Use a verificação: ela confere tanto os testes do aluno quanto um conjunto separado de respostas do runtime.

Amplie a cobertura dos limites e finalize localmente

Nesta etapa, você evitará duas outras regressões: uma entrada inválida chegar à dependência e uma falha do upstream aparecer como uma requisição bem-sucedida. Adicione estes casos sem remover os três anteriores.

O primeiro teste percorre valores JSON inválidos e verifica o status 422; depois, verifica uma entrada de texto não suportada com status 415. Nenhum desses casos deve chamar o upstream. O segundo começa com uma fixture vazia, simula uma resposta 503 da dependência e espera o JSON 502 encapsulado pela API. A comparação do corpo completo da resposta também impede que o diagnóstico interno da fixture seja exposto.

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

Você deve ver cinco testes aprovados e nenhuma falha. Execute a suíte novamente; demo-1 e uma única chamada registrada para a indisponibilidade devem permanecer estáveis, porque o estado da fixture é redefinido a cada caso.

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

Todo o trabalho permaneceu local: as URLs de teste foram despachadas para o Miniflare, todas as chamadas de saída do Worker foram interceptadas e cada runtime foi descartado. Confirme que a VM não tem um login da Cloudflare armazenado:

npx wrangler whoami --json

Você deve esperar "loggedIn": false; o comando de status não autenticado pode terminar com status diferente de zero. Não faça login para este laboratório. Não há recursos na nuvem para excluir. Use a verificação final, que confere a API local real e confirma que seus testes rejeitam cópias defeituosas descartáveis, além de aceitar o código corrigido. Essas cópias de avaliação não modificam seu projeto. Depois, encerre a VM.

Os testes do runtime local tornam as regressões reproduzíveis. Eles não verificam a titularidade da conta, as configurações de deployment, as dependências reais da Internet nem o comportamento da implantação na borda; esses aspectos exigem as verificações remotas do curso.

Resumo

Você escreveu testes que executam a API em um runtime local do Workers, reproduziu uma regressão de 500 em vez de 400 e corrigiu a implementação sem enfraquecer o contrato esperado. Você adicionou casos de entrada inválida e de erro do upstream, redefiniu o estado da fixture entre os casos e descartou cada runtime. A suíte permaneceu local e reproduzível, sem credenciais da nuvem nem gravação de recursos.