Ejecutor de Evaluaciones para n8n
Cuándo activarlo
- Al agregar o modificar lógica de filtros, listas de palabras clave, validadores o mapas de currículum
- Al refactorizar la lógica de funciones puras en un nodo Code
- Antes de fusionar un cambio en un flujo de trabajo — guardia de regresión
- Después de un incidente en producción — codificar la falla en una prueba
- "¿Cómo hago pruebas unitarias para código de n8n?" / "¿Cómo pruebo sin gastar créditos de API?"
Qué es esto (y qué no es)
| Esto es | Esto NO es |
|---|---|
| Un flujo de trabajo de n8n solo de lógica con entradas simuladas | Una prueba de integración contra APIs en vivo |
| Sin llamadas externas excepto el envío del informe final — se ejecuta en ~3 segundos | Una prueba de rendimiento |
| Conteos de aprobados/fallidos/pendientes enviados a Telegram | Un reemplazo para pruebas de humo manuales de flujo completo |
| Una red de regresión para JS puro en nodos Code | Cobertura para el comportamiento de nodos HTTP / Gmail / Telegram |
Costo: sin créditos de API. Solo entradas simuladas en memoria. El informe de Telegram es la única llamada externa.
Arquitectura
[Disparador Manual] → [Ejecutar Todas las Pruebas (nodo Code)] → [Formatear Informe (nodo Code)] → [Enviar Informe (Telegram)]
Un flujo de trabajo por superficie lógica. Se vuelve a ejecutar bajo demanda.
Nodo Code de ejecutor de pruebas estándar
const results = [];
function assert(suite, testName, condition, detail) {
results.push({ suite, test: testName, pass: condition, detail: detail || '' });
}
// ============================================================
// Suite 1 — <nombre>
// ============================================================
// Copiar la función de producción TEXTUALMENTE aquí (no importar — los nodos Code no comparten ámbito)
function passesJs10Filter(job) {
// ... mismo código que el nodo de producción ...
}
// Pruebas
assert('js-10', 'Trabajo de autorización rechazado',
!passesJs10Filter({
title: 'Redactor Técnico',
description: 'Se requiere autorización.',
location: 'remoto'
}),
'la autorización requerida debe provocar el rechazo');
assert('js-10', 'Redactor técnico remoto válido pasa',
passesJs10Filter({
title: 'Redactor Técnico',
description: 'Escribir documentación de API para plataforma en la nube.',
location: 'remoto'
}),
'Un redactor técnico remoto válido debe pasar todos los filtros');
// Para funciones aún no implementadas:
results.push({
suite: 'js-10',
test: 'Límite de antigüedad de 30 días (PENDIENTE — backlog)',
pass: null,
detail: 'Función aún no implementada. Agregar verificación de fecha postedAt, luego volver a ejecutar.'
});
// ============================================================
// Resumir
// ============================================================
const passed = results.filter(r => r.pass === true);
const failed = results.filter(r => r.pass === false);
const pending = results.filter(r => r.pass === null);
return [{ json: {
summary: { total: results.length, passed: passed.length, failed: failed.length, pending: pending.length },
results, failed, pending
}}];
Formateador de informe
const { summary, results } = $input.first().json;
const passIcon = '✅';
const failIcon = '❌';
const pendIcon = '⏳';
const topEmoji = summary.failed === 0 ? '🟢' : (summary.failed <= 3 ? '🟡' : '🔴');
const lines = [
`<b>${topEmoji} <NOMBRE> — Evaluación Lógica</b>`,
`${passIcon} ${summary.passed} aprobados ${failIcon} ${summary.failed} fallidos ${pendIcon} ${summary.pending} pendientes`,
''
];
const suites = {};
for (const r of results) (suites[r.suite] = suites[r.suite] || []).push(r);
for (const [suite, tests] of Object.entries(suites)) {
const suitePass = tests.every(t => t.pass !== false);
lines.push(`<b>${suitePass ? passIcon : failIcon} ${suite}</b>`);
for (const t of tests) {
const icon = t.pass === true ? passIcon : (t.pass === false ? failIcon : pendIcon);
lines.push(` ${icon} ${t.test}`);
if (t.pass !== true) lines.push(` <i>→ ${(t.detail || '').substring(0, 140)}</i>`);
}
lines.push('');
}
const now = new Date().toLocaleString('en-US', {
timeZone: 'UTC', dateStyle: 'short', timeStyle: 'short'
});
lines.push(`<i>Ejecutado: ${now} UTC</i>`);
return [{ json: { message: lines.join('\n') } }];
Nodo de envío Telegram (patrón de variable de entorno, sin credencial n8n)
{
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "keypair",
"bodyParameters": {
"parameters": [
{ "name": "chat_id", "value": "={{ $env.TELEGRAM_CHAT_ID }}" },
{ "name": "text", "value": "={{ $json.message }}" },
{ "name": "parse_mode", "value": "HTML" }
]
}
}
Qué probar (orden de prioridad)
- Cobertura de caminos de rechazo — cada razón por la que un registro puede ser filtrado
- Condiciones de frontera — en el umbral, justo por debajo, justo por encima
- Fallbacks para entrada inválida —
undefined,null,0,'None', tipo incorrecto → valor por defecto esperado - Datos reales conocidos como buenos — al menos un ejemplo de una ejecución pasada confirmada como funcional
- Datos reales conocidos como malos — al menos un ejemplo que causó un incidente pasado
- Coincidencias de subcadena vs exactas — ej.,
'Departamento de RRHH'vs'Departamento de Recursos Humanos' - Funciones pendientes — escritas como
pass: nullpara que aparezcan en los informes como backlog
Convención de nombres de pruebas
<suite>: <afirmación legible para humanos> con el campo detail nombrando la regla subyacente o referencia de error pasado:
assert('js-22', 'Departamento de Recursos Humanos rechazado',
!isValidContact('Departamento de Recursos Humanos'),
'La coincidencia de subcadena BAD_CONTAINS_CONTACT debe detectar esto (corrección de la Sesión 5)');
La regla "convertir errores pasados en pruebas"
Cada entrada en tu Biblioteca de Lecciones Aprendidas que nombre una entrada específica → comportamiento incorrecto es una prueba esperando ser escrita.
Ejemplo de una lección real:
"Aggregate Sector Quotes estaba calculando el cambio de 1 MES en lugar de 1 DÍA porque meta.previousClose devolvía el precio de inicio del gráfico en rango=1mo."
→ Prueba:
assert('aggregate', 'Cambio de 1 día calculado desde closes[], no meta.previousClose',
computeChange({
chart: { result: [{ meta: { previousClose: 100 }, indicators: { quote: [{ close: [100, 98, 99, 105] }] } }] }
}) === ((105 - 99) / 99) * 100,
'meta.previousClose puede ser poco confiable; debe usar closes[-2]');
Cuándo ejecutar
- Antes de cualquier cambio en lógica de filtros, listas de palabras clave, validadores, mapas de currículum
- Después del cambio → confirmar que no hay regresiones
- Después de agregar cualquier entrada a la Biblioteca de Lecciones → escribir la prueba que lo habría detectado
- Pre-despliegue de cualquier nueva versión del flujo de trabajo
Significado de los iconos de estado
| Icono | Significado |
|---|---|
| 🟢 (verde) | 0 fallos — esta suite pasa; despliega solo si tus otras comprobaciones también pasan |
| 🟡 (amarillo) | 1-3 fallos — despliegue bloqueado hasta que se solucione |
| 🔴 (rojo) | 4+ fallos — despliegue bloqueado; probablemente una ruptura más amplia |
Los pendientes (⏳) no cuentan para rojo/amarillo — se rastrean por separado como backlog. Las pruebas pendientes son brechas de cobertura: enuméralas en la nota de despliegue.
Anti-patrones
| Anti-patrón | Qué hacer en su lugar |
|---|---|
| Probar contra APIs en vivo | Solo entradas simuladas — la suite de evaluación es solo lógica |
| Saltar funciones pendientes (perder visibilidad) | Agregar como pass: null con detalle explicando qué falta |
| Importar código de producción | Copiar textualmente — los nodos Code no comparten ámbito |
| Una función de prueba gigante | Un assert() por afirmación — informe legible |
| Descartar la prueba después de corregir un error | Mantenerla para siempre — esa es la guardia de regresión |
| Probar solo el camino feliz | La cobertura de caminos de rechazo son las pruebas de mayor valor |
| Ejecutar solo después de un cambio | Ejecutar ANTES Y DESPUÉS — confirma que no introdujiste regresiones |
| Hardcodear ID de chat / token en el JSON del flujo de trabajo | Usar $env.TELEGRAM_BOT_TOKEN / $env.TELEGRAM_CHAT_ID |
Lista de verificación pre-despliegue
- El flujo de trabajo de evaluación se ejecuta en < 10 segundos
- Todas las suites agrupadas en el informe
- Código de producción copiado textualmente (no parafraseado)
- Al menos 1 prueba por camino de rechazo
- Al menos 1 prueba de "datos reales conocidos como buenos" por suite
- Funciones pendientes representadas como
pass: nullcon detalle - El informe cabe en un mensaje de Telegram (< 4000 caracteres)
Referencia
- Documentación del nodo Code de n8n: https://docs.n8n.io/code/code-node/
- Formato de la API de Telegram Bots: https://core.telegram.org/bots/api#formatting-options