n8n Evaluations Runner
Wann auslösen
- Hinzufügen oder Ändern von Filterlogik, Keyword-Listen, Validatoren oder Resume-Maps
- Refactoring einer reinen Funktionslogik in einem Code-Node
- Vor dem Mergen einer Workflow-Änderung – als Regressionsschutz
- Nach einem Produktionsvorfall – den Fehler in einen Test codieren
- „Wie kann ich n8n-Code unit-testen?“ / „Wie teste ich ohne API-Credits auszugeben?“
Was das ist (und was nicht)
| Das ist | Das ist NICHT |
|---|---|
| Ein logikorientierter n8n-Workflow mit Mock-Eingaben | Ein Integrationstest gegen Live-APIs |
| Keine externen Aufrufe außer dem finalen Berichtsversand – läuft in ~3 Sekunden | Ein Leistungstest |
| Bestanden/Fehlgeschlagen/Ausstehend-Zählungen, die an Telegram gesendet werden | Ein Ersatz für manuelle Smoke-Tests des End-to-End-Flows |
| Ein Regressionsnetz für reines JS in Code-Nodes | Abdeckung für HTTP-/Gmail-/Telegram-Node-Verhalten |
Kosten: keine API-Credits. Nur Mock-Eingaben, nur im Arbeitsspeicher. Der Telegram-Bericht ist der einzige externe Aufruf.
Architektur
[Manueller Auslöser] → [Alle Tests ausführen (Code-Node)] → [Bericht formatieren (Code-Node)] → [Bericht senden (Telegram)]
Ein Workflow pro Logikbereich. Auf Abruf erneut ausführen.
Standard-Testrunner-Code-Node
const results = [];
function assert(suite, testName, condition, detail) {
results.push({ suite, test: testName, pass: condition, detail: detail || '' });
}
// ============================================================
// Suite 1 — <name>
// ============================================================
// Die Produktionsfunktion WÖRTLICH hier kopieren (nicht importieren – Code-Nodes teilen keinen Gültigkeitsbereich)
function passesJs10Filter(job) {
// ... gleicher Code wie im Produktions-Node ...
}
// Tests
assert('js-10', 'Sicherheitsfreigabe-Job abgelehnt',
!passesJs10Filter({
title: 'Technischer Redakteur',
description: 'Sicherheitsfreigabe erforderlich.',
location: 'remote'
}),
'erforderliche Sicherheitsfreigabe muss Ablehnung auslösen');
assert('js-10', 'Gültiger Remote-Techniker besteht',
passesJs10Filter({
title: 'Technischer Redakteur',
description: 'API-Dokumentation für Cloud-Plattform schreiben.',
location: 'remote'
}),
'Gültiger Remote-Technischer Redakteur sollte alle Filter bestehen');
// Für noch nicht implementierte Funktionen:
results.push({
suite: 'js-10',
test: '30-Tage-Altersgrenze (AUSSTEHEND – Rückstand)',
pass: null,
detail: 'Funktion noch nicht implementiert. postedAt-Datumsprüfung hinzufügen, dann erneut ausführen.'
});
// ============================================================
// Zusammenfassen
// ============================================================
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
}}];
Berichtsformatierer
const { summary, results } = $input.first().json;
const passIcon = '✅';
const failIcon = '❌';
const pendIcon = '⏳';
const topEmoji = summary.failed === 0 ? '🟢' : (summary.failed <= 3 ? '🟡' : '🔴');
const lines = [
`<b>${topEmoji} <NAME> — Logik-Evaluierung</b>`,
`${passIcon} ${summary.passed} bestanden ${failIcon} ${summary.failed} fehlgeschlagen ${pendIcon} ${summary.pending} ausstehend`,
''
];
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>Ausführung: ${now} UTC</i>`);
return [{ json: { message: lines.join('\n') } }];
Telegram-Sende-Node (Umgebungsvariablen-Muster, kein n8n-Credential)
{
"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" }
]
}
}
Was testen (Prioritätsreihenfolge)
- Ablehnungspfad-Abdeckung — jeder Grund, warum ein Datensatz herausgefiltert werden kann
- Grenzfälle — genau am Schwellwert, knapp darunter, knapp darüber
- Ungültige-Eingabe-Fallbacks —
undefined,null,0,'None', falscher Typ → erwarteter Standardwert - Bekannte gültige Echtzeitdaten — mindestens ein Beispiel aus einer früheren bestätigt funktionierenden Ausführung
- Bekannte fehlerhafte Echtzeitdaten — mindestens ein Beispiel, das einen früheren Vorfall verursacht hat
- Teilstring- vs. exakte Matcher — z. B.
'HR-Abteilung'vs.'Personalabteilung' - Ausstehende Funktionen — als
pass: nullgeschrieben, damit sie in Berichten als Rückstand erscheinen
Testbenennungskonvention
<suite>: <menschenlesbare Behauptung> mit dem Detailfeld, das die zugrunde liegende Regel oder den vergangenen Bug-Referenz benennt:
assert('js-22', 'Personalabteilung abgelehnt',
!isValidContact('Personalabteilung'),
'BAD_CONTAINS_CONTACT-Teilstring-Matching muss dies abfangen (Session-5-Bugfix)');
Die Regel „Vergangene Bugs in Tests umwandeln“
Jeder Eintrag in Ihrer Lessons Learned Library, der eine bestimmte Eingabe → falsches Verhalten nennt, ist ein Test, der darauf wartet, geschrieben zu werden.
Beispiel aus einer echten Lektion:
„Aggregate Sector Quotes hat die 1-MONATS-Änderung statt der 1-TAGES-Änderung berechnet, weil meta.previousClose den Chart-Startpreis bei Bereich=1mo zurückgab.“
→ Test:
assert('aggregate', '1-Tages-Änderung aus closes[] berechnet, nicht aus meta.previousClose',
computeChange({
chart: { result: [{ meta: { previousClose: 100 }, indicators: { quote: [{ close: [100, 98, 99, 105] }] } }] }
}) === ((105 - 99) / 99) * 100,
'meta.previousClose kann unzuverlässig sein; muss closes[-2] verwenden');
Wann ausführen
- Vor jeder Änderung an Filterlogik, Keyword-Listen, Validatoren, Resume-Maps
- Nach der Änderung → bestätigen, dass keine Regressionen aufgetreten sind
- Nachdem ein LL-Library-Eintrag hinzugefügt wurde → den Test schreiben, der ihn abgefangen hätte
- Vor dem Deployment einer neuen Workflow-Version
Bedeutung der Status-Icons
| Icon | Bedeutung |
|---|---|
| 🟢 (grün) | 0 Fehlschläge – diese Suite besteht; deployen Sie nur, wenn Ihre anderen Prüfungen ebenfalls bestanden werden |
| 🟡 (gelb) | 1-3 Fehlschläge – Deployment blockiert, bis behoben |
| 🔴 (rot) | 4+ Fehlschläge – Deployment blockiert; wahrscheinlich ein breiterer Bruch |
Ausstehend (⏳) zählt nicht zu Rot/Gelb – es wird separat als Rückstand verfolgt. Ausstehende Tests sind Abdeckungslücken: Listen Sie sie im Deployment-Hinweis auf.
Anti-Patterns
| Anti-Pattern | Stattdessen tun |
|---|---|
| Gegen Live-APIs testen | Nur Mock-Eingaben – Evaluierungssuite ist nur Logik |
| Ausstehende Funktionen überspringen (Sichtbarkeit verlieren) | Als pass: null mit Erklärung hinzufügen, was fehlt |
| Produktionscode importieren | Wörtlich kopieren – Code-Nodes teilen keinen Gültigkeitsbereich |
| Eine riesige Testfunktion | Ein assert() pro Behauptung – lesbarer Bericht |
| Test nach Bugfix verwerfen | Für immer behalten – das ist der Regressionsschutz |
| Nur Happy Path testen | Ablehnungspfad-Abdeckung sind die Tests mit dem höchsten Wert |
| Nur nach einer Änderung ausführen | Vor UND nach der Änderung ausführen – bestätigt, dass Sie keine Regressionen eingeführt haben |
| Chat-ID/Token in Workflow-JSON hartcodieren | $env.TELEGRAM_BOT_TOKEN / $env.TELEGRAM_CHAT_ID verwenden |
Pre-Deploy-Checkliste
- Evaluierungs-Workflow läuft in < 10 Sekunden
- Alle Suiten sind im Bericht gruppiert
- Produktionscode wörtlich kopiert (nicht paraphrasiert)
- Mindestens 1 Test pro Ablehnungspfad
- Mindestens 1 „bekannte gültige Echtzeitdaten“-Test pro Suite
- Ausstehende Funktionen als
pass: nullmit Detail dargestellt - Bericht passt in eine Telegram-Nachricht (< 4000 Zeichen)
Referenz
- n8n-Code-Node-Dokumentation: https://docs.n8n.io/code/code-node/
- Telegram-Bot-API-Formatierung: https://core.telegram.org/bots/api#formatting-options