Executor de Avaliações n8n
Quando acionar
- Ao adicionar ou modificar lógica de filtros, listas de palavras-chave, validadores ou mapas de currículo
- Ao refatorar a lógica de função pura de um nó Code
- Antes de mesclar uma alteração no fluxo de trabalho — guarda de regressão
- Após um incidente de produção — codificar a falha em um teste
- "Como faço testes unitários para código n8n?" / "Como testar sem gastar créditos de API?"
O que isso é (e não é)
| Isso é | Isso NÃO é |
|---|---|
| Um fluxo de trabalho n8n apenas lógico com entradas simuladas | Um teste de integração contra APIs ativas |
| Sem chamadas externas, exceto o envio do relatório final — executa em ~3 segundos | Um teste de desempenho |
| Contagens de passou/falhou/pendente enviadas para o Telegram | Um substituto para testes manuais de fumaça de fluxo ponta a ponta |
| Uma rede de regressão para JS puro em nós Code | Cobertura para comportamento de nós HTTP / Gmail / Telegram |
Custo: sem créditos de API. Entradas simuladas, apenas em memória. O relatório do Telegram é a única chamada externa.
Arquitetura
[Acionador Manual] → [Executar Todos os Testes (nó Code)] → [Formatar Relatório (nó Code)] → [Enviar Relatório (Telegram)]
Um fluxo de trabalho por superfície lógica. Execute sob demanda.
Nó Code padrão do executor de testes
const results = [];
function assert(suite, testName, condition, detail) {
results.push({ suite, test: testName, pass: condition, detail: detail || '' });
}
// ============================================================
// Suíte 1 — <nome>
// ============================================================
// Copie a função de produção VERBATIM aqui (não importe — nós Code não compartilham escopo)
function passaFiltroJs10(job) {
// ... mesmo código do nó de produção ...
}
// Testes
assert('js-10', 'Trabalho com autorização rejeitado',
!passaFiltroJs10({
title: 'Redator Técnico',
description: 'Autorização necessária.',
location: 'remoto'
}),
'autorização necessária deve disparar rejeição');
assert('js-10', 'Redator técnico remoto válido passa',
passaFiltroJs10({
title: 'Redator Técnico',
description: 'Escrever documentação de API para plataforma em nuvem.',
location: 'remoto'
}),
'Redator técnico remoto válido deve passar em todos os filtros');
// Para funcionalidades ainda não implementadas:
results.push({
suite: 'js-10',
test: 'Limite de idade de 30 dias (PENDENTE — backlog)',
pass: null,
detail: 'Funcionalidade ainda não implementada. Adicione verificação de data postedAt e execute novamente.'
});
// ============================================================
// Resumo
// ============================================================
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
}}];
Formatador de relatório
const { summary, results } = $input.first().json;
const passIcon = '✅';
const failIcon = '❌';
const pendIcon = '⏳';
const topEmoji = summary.failed === 0 ? '🟢' : (summary.failed <= 3 ? '🟡' : '🔴');
const lines = [
`<b>${topEmoji} <NOME> — Avaliação Lógica</b>`,
`${passIcon} ${summary.passed} passaram ${failIcon} ${summary.failed} falharam ${pendIcon} ${summary.pending} pendentes`,
''
];
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('pt-BR', {
timeZone: 'UTC', dateStyle: 'short', timeStyle: 'short'
});
lines.push(`<i>Execução: ${now} UTC</i>`);
return [{ json: { message: lines.join('\n') } }];
Nó de envio do Telegram (padrão de variável de ambiente, sem 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" }
]
}
}
O que testar (ordem de prioridade)
- Cobertura de caminho de rejeição — toda razão pela qual um registro pode ser filtrado
- Condições de limite — no limite, logo abaixo, logo acima
- Fallbacks para entrada inválida —
undefined,null,0,'Nenhum', tipo errado → padrão esperado - Dados reais conhecidos como bons — pelo menos um exemplo de uma execução anterior confirmada como funcional
- Dados reais conhecidos como ruins — pelo menos um exemplo que causou um incidente anterior
- Correspondência de substring vs exata — por exemplo,
'Departamento de RH'vs'Departamento de Recursos Humanos' - Funcionalidades pendentes — escritas como
pass: nullpara aparecerem nos relatórios como backlog
Convenção de nomenclatura de testes
<suíte>: <asserção legível por humano> com o campo de detalhe nomeando a regra subjacente ou referência de bug passado:
assert('js-22', 'Departamento de Recursos Humanos rejeitado',
!isValidContact('Departamento de Recursos Humanos'),
'A correspondência de substring BAD_CONTAINS_CONTACT deve capturar isso (correção do bug da Sessão 5)');
A regra de "converter bugs passados em testes"
Cada entrada em sua Biblioteca de Lições Aprendidas que nomeia uma entrada específica → comportamento errado é um teste esperando para ser escrito.
Exemplo de uma lição real:
"O Agregador de Cotações de Setor estava calculando a variação de 1 MÊS em vez de 1 DIA porque meta.previousClose retornava o preço de início do gráfico no intervalo=1mo."
→ Teste:
assert('aggregate', 'Variação de 1 dia calculada a partir de closes[], não de meta.previousClose',
computeChange({
chart: { result: [{ meta: { previousClose: 100 }, indicators: { quote: [{ close: [100, 98, 99, 105] }] } }] }
}) === ((105 - 99) / 99) * 100,
'meta.previousClose pode não ser confiável; deve usar closes[-2]');
Quando executar
- Antes de qualquer alteração na lógica de filtros, listas de palavras-chave, validadores, mapas de currículo
- Após a alteração → confirmar que não há regressões
- Após a adição de qualquer entrada na Biblioteca de Lições → escrever o teste que a teria capturado
- Pré-implantação de qualquer nova versão do fluxo de trabalho
Significado dos ícones de status
| Ícone | Significado |
|---|---|
| 🟢 (verde) | 0 falhas — esta suíte passa; implante somente se suas outras verificações também passarem |
| 🟡 (amarelo) | 1-3 falhas — implantação bloqueada até correção |
| 🔴 (vermelho) | 4+ falhas — implantação bloqueada; provável quebra mais ampla |
Pendentes (⏳) não contam para vermelho/amarelo — são rastreados separadamente como backlog. Testes pendentes são lacunas de cobertura: liste-os na nota de implantação.
Antipadrões
| Antipadrão | O que fazer em vez disso |
|---|---|
| Testar contra APIs ativas | Apenas entradas simuladas — a suíte de avaliação é apenas lógica |
| Pular funcionalidades pendentes (perder visibilidade) | Adicionar como pass: null com detalhe explicando o que está faltando |
| Importar código de produção | Copiar verbatim — nós Code não compartilham escopo |
| Uma função de teste gigante | Um assert() por asserção — relatório legível |
| Descartar o teste após corrigir o bug | Mantê-lo para sempre — essa é a guarda de regressão |
| Testar apenas o caminho feliz | Cobertura de caminho de rejeição são os testes de maior valor |
| Executar apenas após uma alteração | Executar ANTES E DEPOIS — confirma que você não introduziu regressões |
| Codificar ID do chat / token no JSON do fluxo | Usar $env.TELEGRAM_BOT_TOKEN / $env.TELEGRAM_CHAT_ID |
Lista de verificação pré-implantação
- Fluxo de trabalho de avaliação executa em < 10 segundos
- Todas as suítes agrupadas no relatório
- Código de produção copiado verbatim (não parafraseado)
- Pelo menos 1 teste por caminho de rejeição
- Pelo menos 1 teste de "dados reais conhecidos como bons" por suíte
- Funcionalidades pendentes representadas como
pass: nullcom detalhe - Relatório cabe em uma mensagem do Telegram (< 4000 caracteres)
Referência
- Documentação do nó Code do n8n: https://docs.n8n.io/code/code-node/
- Formatação da API de Bot do Telegram: https://core.telegram.org/bots/api#formatting-options