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.

