Testar Workers com Jest rodando em Node.js é uma armadilha elegante. Os testes passam, o CI fica verde, e o código chega em produção com bugs que o ambiente de testes nunca poderia detectar — porque o ambiente de testes não é o runtime que executa o código em produção. Workers rodam em V8 isolates com as APIs da plataforma web, não com as APIs do Node.js: não há Buffer, não há process.env, o fetch é a implementação nativa do runtime (não o node-fetch nem o undici), e crypto é a Web Crypto API com suas assinaturas assíncronas. Um mock de fetch escrito para Jest em Node.js pode passar nos testes e silenciosamente não funcionar quando o Worker está lidando com o Response real do edge.
Por que o ambiente de teste importa mais do que parece
A diferença entre Node.js e o runtime de Workers vai além das APIs disponíveis: o comportamento de borda só aparece sob condições específicas. O TextEncoder em Node.js e o TextEncoder no runtime de Workers têm a mesma interface, mas o Uint8Array que produzem pode se comportar diferente quando passado para uma SubtleCrypto operation dependendo de versão e implementação. O Headers do Workers tem um comportamento de normalização de nomes de header (case-insensitive, ordem lexicográfica) que difere de implementações de mock.
Mais importante: bindings não existem em Node.js. env.KV, env.DB, env.BUCKET são objetos que o runtime injeta — não há como importar um pacote npm que os forneça com fidelidade completa. Mocks manuais testam o código que chama o mock, não o código que vai funcionar com o binding real.
O @cloudflare/vitest-pool-workers resolve isso executando os testes dentro de um Workers runtime real — especificamente Miniflare v3, que roda um V8 isolate de verdade, não uma simulação em Node.js. Os tests têm acesso a caches, crypto, fetch, Headers, ReadableStream, TransformStream com as implementações do runtime. Bindings são fornecidos como implementações in-process: KV em memória, D1 como SQLite embutido, R2 como armazenamento local. O resultado é que um teste que passa com vitest-pool-workers está testando código que vai funcionar no runtime real.
Configurando o vitest.config.ts com bindings
A configuração do pool de Workers no vitest.config.ts é onde você declara os bindings que os testes vão receber. A estrutura espelha o wrangler.toml, mas na configuração do Vitest:
import { defineConfig } from 'vitest/config'; import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' }, miniflare: { kvNamespaces: ['CACHE'], d1Databases: ['DB'], r2Buckets: ['BUCKET'], bindings: { APP_ENV: 'test', }, }, }, }, }, });
Com essa configuração, cada suite de testes recebe um ambiente limpo com um namespace KV vazio, um banco D1 SQLite vazio (com o schema aplicado se você configurar migrations), e um bucket R2 vazio. Os bindings são isolados entre suites — mudanças feitas pelos testes de uma suite não vazam para outra.
Para acessar os bindings nos testes, o @cloudflare/vitest-pool-workers exporta um helper env que o Miniflare injeta via um mecanismo global:
import { env } from 'cloudflare:test'; test('deve retornar 404 para usuário inexistente', async () => { const request = new Request('https://api.example.com/users/999'); const response = await SELF.fetch(request); expect(response.status).toBe(404); });
O SELF é o binding do próprio Worker em teste — você pode fazer SELF.fetch() para testar o handler de fetch como uma chamada HTTP de integração, com routing completo e todos os bindings disponíveis.
A divisão entre testes unitários e de integração
Funções de lógica de negócio que operam sobre valores JavaScript puros — parsear um JWT, calcular um preço com desconto, validar um schema de entrada — podem e devem ser testadas sem o pool de Workers. Essas funções não dependem de APIs do runtime nem de bindings, e testá-las em Node.js puro com Vitest padrão é mais rápido e mais simples. A regra prática: se a função recebe e retorna valores JavaScript primitivos ou objetos planos, ela pertence ao pool Node.js. Se ela usa Request, Response, Headers, ReadableStream, ou qualquer binding, ela pertence ao pool de Workers.
Handlers HTTP de integração — a função fetch(request, env, ctx) que é o entry point do Worker — precisam do pool de Workers para ser testados com fidelidade. Testar um handler de integração significa fazer SELF.fetch(new Request(...)) e verificar o status, os headers de resposta, e o body da resposta que o Worker produziria em produção. Esse teste também exercita o efeito colateral nos bindings: se o handler deveria salvar um registro no D1, o teste pode fazer env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first() depois e verificar que o dado foi escrito corretamente.
CI sem credenciais Cloudflare — e os limites do Miniflare
O vitest-pool-workers com Miniflare roda completamente local — não faz nenhuma chamada para a API da Cloudflare, não precisa de CLOUDFLARE_ACCOUNT_ID nem de CLOUDFLARE_API_TOKEN no CI. O Miniflare v3 baixa o workerd (o runtime open-source da Cloudflare) como binário local e o executa como qualquer outro processo. O pipeline de CI se resume a npm ci e npx vitest run — sem segredos, sem dependência de rede além do download do binário na primeira instalação.
O Miniflare v3 simula o runtime com fidelidade alta, mas há exceções documentadas que importam para a estratégia de testes. Durable Objects em modo de produção — com sua garantia de singleton global — não são simulados com consistência distribuída; o Miniflare cria uma instância local útil para lógica básica, mas não testa o comportamento de coordenação real. Comportamentos do CDN como cache de borda e o objeto cf com dados de geolocalização reais precisam do wrangler dev --remote para ser exercitados.
Para esses casos, a resposta são testes de fumaça em staging que fazem chamadas HTTP diretas para um Worker de staging após o deploy. Eles não substituem os testes com Miniflare — cobrem o que o Miniflare não pode simular: comportamentos que só existem quando o código roda na infraestrutura real.
Testando waitUntil e background work
O ctx.waitUntil() é onde trabalho acontece depois que a resposta foi enviada. O SELF.fetch() nos testes retorna a resposta imediatamente, mas o trabalho passado para waitUntil pode não ter terminado. O @cloudflare/vitest-pool-workers fornece waitOnExecutionContext para resolver isso:
import { env, SELF, waitOnExecutionContext } from 'cloudflare:test'; test('deve enfileirar analytics em background', async () => { const ctx = createExecutionContext(); const response = await SELF.fetch(new Request('https://api.example.com/checkout')); await waitOnExecutionContext(ctx); const queued = await env.ANALYTICS_QUEUE.read(); expect(queued).toHaveLength(1); });
Sem waitOnExecutionContext, um teste que verifica o efeito colateral de waitUntil vai ter uma race condition — flakiness difícil de diagnosticar porque o problema não está no código de produção, está no código de teste.
Leia também
- Cloudflare Workers em produção: o que muda depois do hello world
- Workers + D1 + KV + R2: compondo bindings num mesmo serviço
- Workers: debugging, logs e Workers Tail — observabilidade no edge sem servidor de log
- Workers: limites de CPU e memória — o que a documentação não explica direito
- Desenvolvendo aplicações serverless com AWS Lambda e Cloudflare Workers em 2025
- Cloudflare Workers: Guia Prático para Serverless Edge Computing