Teste seu Worker localmente

CloudflareBeginner
Pratique Agora

Introdução

Tentar uma requisição válida uma vez não mostra como uma API lida com erros. Testes automatizados repetem requisições e comparam os resultados com o que a aplicação promete, facilitando encontrar comportamentos quebrados antes da implantação. Aqui, uma API de suporte falha com JSON malformado. Você escreverá um teste que revela o defeito, corrigirá o problema e manterá as verificações do comportamento que já funciona.

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. Um caso de teste descreve uma situação e o resultado esperado; uma asserção faz a comparação e falha se os valores forem diferentes. O runner de testes do Node organiza os casos. O Miniflare cria um ambiente local do Workers usando o workerd, o runtime do Workers, para que as requisições exercitem o comportamento do Worker em vez de tratar o handler como uma função comum do 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.

Os hooks de teste executam preparação ou limpeza ao redor de cada caso. Dar a cada caso um estado novo impede que as requisições de um teste alterem o resultado de outro. 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ê transformará o defeito relatado de JSON malformado em um teste de regressão: uma verificação repetível que avisará se o mesmo problema voltar após uma edição futura. A API deve responder com 400, mas o handler inicial deixa uma exceção de análise escapar. Ver o novo teste falhar primeiro prova que ele consegue detectar o problema que você pretende corrigir.

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.