Exécuteur d'évaluations n8n
Quand déclencher
- Ajout ou modification d'une logique de filtre, de listes de mots-clés, de validateurs ou de cartes de résumé
- Refactorisation de la logique d'une fonction pure dans un Code node
- Avant de fusionner une modification de workflow — garde de régression
- Après un incident de production — transformer l'échec en test
- « Comment effectuer des tests unitaires du code n8n ? » / « Comment tester sans utiliser de crédits API ? »
Ce que c'est (et ce que ce n'est pas)
| Ceci est | Ceci n'est PAS |
|---|---|
| Un workflow n8n uniquement logique avec des entrées simulées | Un test d'intégration contre des API en production |
| Aucun appel externe sauf l'envoi final du rapport — s'exécute en ~3 secondes | Un test de performance |
| Des compteurs réussite/échec/en attente envoyés à Telegram | Un remplacement des tests de fumée manuels du flux de bout en bout |
| Un filet de régression pour le JS pur dans les Code nodes | Une couverture pour le comportement des nœuds HTTP / Gmail / Telegram |
Coût : aucun crédit API. Entrées simulées, uniquement en mémoire. L'envoi du rapport Telegram est le seul appel externe.
Architecture
[Déclencheur manuel] → [Exécuter tous les tests (Code node)] → [Formater le rapport (Code node)] → [Envoyer le rapport (Telegram)]
Un workflow par surface logique. Réexécution à la demande.
Nœud Code standard du moteur de test
const results = [];
function assert(suite, testName, condition, detail) {
results.push({ suite, test: testName, pass: condition, detail: detail || '' });
}
// ============================================================
// Suite 1 — <nom>
// ============================================================
// Copiez la fonction de production LITTÉRALEMENT ici (n'importez pas — les Code nodes ne partagent pas la portée)
function passesJs10Filter(job) {
// ... même code que le nœud de production ...
}
// Tests
assert('js-10', 'Offre d'emploi avec habilitation rejetée',
!passesJs10Filter({
title: 'Rédacteur technique',
description: 'Habilitation requise.',
location: 'télétravail'
}),
'l'habilitation requise doit déclencher le rejet');
assert('js-10', 'Rédacteur technique valide en télétravail passe',
passesJs10Filter({
title: 'Rédacteur technique',
description: 'Rédiger la documentation API pour une plateforme cloud.',
location: 'télétravail'
}),
'Un rédacteur technique valide en télétravail doit passer tous les filtres');
// Pour les fonctionnalités non encore implémentées :
results.push({
suite: 'js-10',
test: 'Limite d'âge de 30 jours (EN ATTENTE — backlog)',
pass: null,
detail: 'Fonctionnalité pas encore implémentée. Ajouter la vérification de la date postedAt, puis réexécuter.'
});
// ============================================================
// Résumé
// ============================================================
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
}}];
Formateur de rapport
const { summary, results } = $input.first().json;
const passIcon = '✅';
const failIcon = '❌';
const pendIcon = '⏳';
const topEmoji = summary.failed === 0 ? '🟢' : (summary.failed <= 3 ? '🟡' : '🔴');
const lines = [
`<b>${topEmoji} <NOM> — Évaluation logique</b>`,
`${passIcon} ${summary.passed} réussis ${failIcon} ${summary.failed} échoués ${pendIcon} ${summary.pending} en attente`,
''
];
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('fr-FR', {
timeZone: 'UTC', dateStyle: 'short', timeStyle: 'short'
});
lines.push(`<i>Exécuté : ${now} UTC</i>`);
return [{ json: { message: lines.join('\n') } }];
Nœud d'envoi Telegram (modèle avec variables d'environnement, sans identifiant 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" }
]
}
}
Ce qu'il faut tester (par ordre de priorité)
- Couverture des chemins de rejet — chaque raison pour laquelle un enregistrement peut être filtré
- Conditions limites — au seuil, juste en dessous, juste au-dessus
- Gestion des entrées invalides —
undefined,null,0,'None', mauvais type → valeur par défaut attendue - Données réelles connues comme valides — au moins un exemple d'une exécution passée confirmée
- Données réelles connues comme invalides — au moins un exemple ayant causé un incident passé
- Correspondance de sous-chaîne vs exacte — par exemple,
'Service RH'vs'Département des Ressources Humaines' - Fonctionnalités en attente — écrites avec
pass: nullpour apparaître dans les rapports comme backlog
Convention de nommage des tests
<suite> : <assertion lisible> avec le champ detail nommant la rgle sous-jacente ou la référence de l'ancien bug :
assert('js-22', 'Département des Ressources Humaines rejeté',
!isValidContact('Département des Ressources Humaines'),
'La correspondance de sous-chaîne BAD_CONTAINS_CONTACT doit intercepter cela (correction du bug de la Session 5)');
La règle « convertir les bugs passés en tests »
Chaque entrée de votre Bibliothèque de leçons apprises qui nomme une entrée spécifique → un comportement erroné est un test qui attend d'être écrit.
Exemple tiré d'une leçon réelle :
« Aggregate Sector Quotes calculait la variation sur 1 MOIS au lieu de 1 JOUR car meta.previousClose renvoyait le prix de début du graphique pour un intervalle de 1 mois. »
→ Test :
assert('aggregate', 'variation sur 1 jour calculée à partir de closes[], pas de meta.previousClose',
computeChange({
chart: { result: [{ meta: { previousClose: 100 }, indicators: { quote: [{ close: [100, 98, 99, 105] }] } }] }
}) === ((105 - 99) / 99) * 100,
'meta.previousClose peut être peu fiable ; doit utiliser closes[-2]');
Quand exécuter
- Avant toute modification de la logique de filtre, des listes de mots-clés, des validateurs, des cartes de résumé
- Après la modification → confirmer l'absence de régressions
- Après l'ajout d'une entrée dans la Bibliothèque de leçons → écrire le test qui l'aurait interceptée
- Pré-déploiement de toute nouvelle version du workflow
Signification des icônes de statut
| Icône | Signification |
|---|---|
| 🟢 (vert) | 0 échecs — cette suite passe ; déployez uniquement si vos autres vérifications passent aussi |
| 🟡 (jaune) | 1 à 3 échecs — déploiement bloqué jusqu'à correction |
| 🔴 (rouge) | 4 échecs ou plus — déploiement bloqué ; probablement une rupture plus large |
Les tests en attente (⏳) ne sont pas comptés dans le rouge/jaune — ils sont suivis séparément comme backlog. Les tests en attente sont des lacunes de couverture : listez-les dans la note de déploiement.
Anti-patrons
| Anti-patron | Que faire à la place |
|---|---|
| Tester contre des API en production | Entrées simulées uniquement — la suite d'évaluation est purement logique |
| Ignorer les fonctionnalités en attente (perte de visibilité) | Ajouter avec pass: null et un détail expliquant ce qui manque |
| Importer le code de production | Copier littéralement — les Code nodes ne partagent pas la portée |
| Une fonction de test géante | Une assert() par assertion — rapport lisible |
| Supprimer le test après la correction d'un bug | Le conserver pour toujours — c'est la garde de régression |
| Tester uniquement le scénario idéal | La couverture des chemins de rejet est le test à plus forte valeur ajoutée |
| Exécuter uniquement après une modification | Exécuter AVANT ET APRÈS — confirme que vous n'avez pas introduit de régressions |
| Coder en dur l'ID de chat / le jeton dans le JSON du workflow | Utiliser $env.TELEGRAM_BOT_TOKEN / $env.TELEGRAM_CHAT_ID |
Liste de vérification pré-déploiement
- Le workflow d'évaluation s'exécute en moins de 10 secondes
- Toutes les suites sont regroupées dans le rapport
- Le code de production est copié littéralement (pas paraphrasé)
- Au moins 1 test par chemin de rejet
- Au moins 1 test « données réelles connues valides » par suite
- Les fonctionnalités en attente sont représentées avec
pass: nullet un détail - Le rapport tient dans un seul message Telegram (< 4000 caractères)
Références
- Documentation du Code node n8n : https://docs.n8n.io/code/code-node/
- Mise en forme de l'API Telegram Bot : https://core.telegram.org/bots/api#formatting-options