Cloudflare Workers
Testing
Vitest
Miniflare
CI/CD

Testing Workers: unidad, integración y cómo simular el tiempo de ejecución sin depender de Cloudflare

El tiempo de ejecución de Workers no es Node.js: las pruebas sin tener esto en cuenta garantizan que las pruebas se realicen localmente y que aparezcan errores en producción.

Testing Workers: unidad, integración y cómo simular el tiempo de ejecución sin depender de Cloudflare

Probar Workers con Jest ejecutándose en Node.js es una trampa elegante. Las pruebas pasan, el CI se vuelve verde y el código llega a producción con errores que el entorno de prueba nunca pudo detectar, porque el entorno de prueba no es el tiempo de ejecución que ejecuta el código en producción. Los trabajadores ejecutan aislados V8 con las API de la plataforma web, no con las API de Node.js: no hay Buffer, no hay process.env, el fetch es la implementación nativa del tiempo de ejecución (ni el node-fetch ni el undici) y el crypto es la API Web Crypto con sus firmas asincrónicas. Un simulacro de fetch escrito para Jest en Node.js puede pasar las pruebas y no funcionar silenciosamente cuando el Trabajador está lidiando con el Response real desde el borde.

Por qué el entorno de pruebas importa más de lo que parece

La diferencia entre Node.js y el tiempo de ejecución de Workers va más allá de las API disponibles: el comportamiento de borde solo aparece bajo condiciones específicas. El TextEncoder en Node.js y el TextEncoder en el tiempo de ejecución de Workers tienen la misma interfaz, pero el Uint8Array que producen puede comportarse de manera diferente cuando se pasa a una operación SubtleCrypto dependiendo de la versión y la implementación. Workers Headers tiene un comportamiento de normalización de nombres de encabezado (sin distinguir entre mayúsculas y minúsculas, orden lexicográfico) que difiere de las implementaciones simuladas.

Lo más importante: los enlaces no existen en Node.js. env.KV, env.DB, env.BUCKET son objetos que el tiempo de ejecución inyecta; no hay forma de importar un paquete npm que les proporcione total fidelidad. Los simulacros manuales prueban el código que llama al simulacro, no el código que funcionará con el enlace real.

@cloudflare/vitest-pool-workers resuelve esto ejecutando las pruebas dentro de un tiempo de ejecución de Workers real, específicamente Miniflare v3, que ejecuta un aislamiento V8 real, no una simulación de Node.js. Las pruebas tienen acceso a caches, crypto, fetch, Headers, ReadableStream, TransformStream con implementaciones de tiempo de ejecución. Los enlaces se proporcionan como implementaciones en proceso: KV en memoria, D1 como SQLite integrado, R2 como almacenamiento local. El resultado es que una prueba que pasa con vitest-pool-workers está probando código que funcionará en tiempo de ejecución real.

Configurando vitest.config.ts con enlaces

La configuración del grupo de trabajadores en vitest.config.ts es donde declara los enlaces que recibirán las pruebas. La estructura refleja wrangler.toml, pero en la configuración de 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', }, }, }, }, }, });

Con esta configuración, cada conjunto de pruebas recibe un entorno limpio con un espacio de nombres KV vacío, una base de datos SQLite D1 vacía (con el esquema aplicado si configura migrations) y un depósito R2 vacío. Los enlaces están aislados entre conjuntos: los cambios realizados por las pruebas en un conjunto no se filtran a otro.

Para acceder a los enlaces en las pruebas, @cloudflare/vitest-pool-workers exporta un ayudante env que Miniflare inyecta a través de un 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); });

SELF es el enlace del propio trabajador bajo prueba; puede hacer SELF.fetch() para probar el controlador de recuperación como una llamada HTTP de integración, con enrutamiento completo y todos los enlaces disponibles.

La división entre pruebas unitarias y de integración

Las funciones de lógica empresarial que operan con valores puros de JavaScript (analizar un JWT, calcular un precio con descuento, validar un esquema de entrada) pueden y deben probarse sin el grupo de trabajadores. Estas funciones no dependen de enlaces o API de tiempo de ejecución, y probarlas en Node.js puro con Vitest estándar es más rápido y sencillo. La regla general: si la función toma y devuelve valores primitivos de JavaScript u objetos planos, pertenece al grupo de Node.js. Si utiliza Request, Response, Headers, ReadableStream o cualquier enlace, pertenece al grupo de trabajadores.

Los controladores HTTP de integración (la función fetch(request, env, ctx) que es el punto de entrada de los trabajadores) necesitan que el grupo de trabajadores se pruebe fielmente. Probar un controlador de integración significa ir al SELF.fetch(new Request(...)) y verificar el estado, los encabezados de respuesta y el cuerpo de la respuesta que el Trabajador produciría en producción. Esta prueba también tiene un efecto secundario en los enlaces: si el controlador debe guardar un registro en D1, la prueba puede realizar env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first() más tarde y verificar que los datos se escribieron correctamente.

CI sin credenciales de Cloudflare y los límites de Miniflare

vitest-pool-workers con Miniflare se ejecuta de forma completamente local: no realiza ninguna llamada a la API de Cloudflare, no necesita CLOUDFLARE_ACCOUNT_ID o CLOUDFLARE_API_TOKEN en el CI. Miniflare v3 descarga workerd (el tiempo de ejecución de código abierto de Cloudflare) como un binario local y lo ejecuta como cualquier otro proceso. La canalización de CI se reduce a npm ci y npx vitest run: sin secretos, sin dependencia de red más allá de la descarga binaria en la primera instalación.

Miniflare v3 simula el tiempo de ejecución con alta fidelidad, pero existen excepciones documentadas que son importantes para la estrategia de prueba. Los objetos duraderos en modo de producción, con su garantía global única, no se simulan con coherencia distribuida; Miniflare crea una instancia local útil para la lógica básica, pero no prueba el comportamiento de coordinación real. Es necesario ejercitar los comportamientos de CDN como el almacenamiento en caché de borde y el objeto cf con datos de geolocalización reales.

Para estos casos, la respuesta es realizar pruebas de humo que realicen llamadas HTTP directas a un trabajador provisional después de la implementación. No reemplazan las pruebas de Miniflare; cubren lo que Miniflare no puede simular: comportamientos que solo existen cuando el código se ejecuta en una infraestructura real.

Prueba de espera hasta y trabajo en segundo plano

El ctx.waitUntil() es donde se trabaja después de que se ha enviado la respuesta. El SELF.fetch() en las pruebas devuelve la respuesta inmediatamente, pero es posible que el trabajo pasado al waitUntil no esté terminado. @cloudflare/vitest-pool-workers proporciona waitOnExecutionContext para resolver esto:

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); });

Sin waitOnExecutionContext, una prueba que verifique el efecto secundario de waitUntil tendrá una condición de carrera: descamación que es difícil de diagnosticar porque el problema no está en el código de producción, sino en el código de prueba.

Lea también