n8n 평가 러너
트리거 시점
- 필터 로직, 키워드 목록, 검증기 또는 이력서 맵을 추가/수정할 때
- 코드 노드의 순수 함수 로직을 리팩터링할 때
- 워크플로우 변경을 병합하기 전 — 회귀 방지
- 프로덕션 장애 후 — 실패한 부분을 테스트로 코드화
- "n8n 코드를 단위 테스트하려면 어떻게 해야 하나요?" / "API 크레딧을 사용하지 않고 테스트하려면 어떻게 해야 하나요?"
이것의 정체 (그리고 아닌 것)
| 이것은 | 이것은 아닙니다 |
|---|---|
| 모의 입력을 사용한 로직 전용 n8n 워크플로우 | 실시간 API에 대한 통합 테스트 |
| 최종 보고서 전송 외에는 외부 호출 없음 — 약 3초 내 실행 | 성능 테스트 |
| Telegram으로 푸시되는 통과/실패/보류 개수 | 엔드투엔드 흐름의 수동 스모크 테스트 대체 |
| 코드 노드의 순수 JS를 위한 회귀 방지망 | HTTP / Gmail / Telegram 노드 동작에 대한 커버리지 |
비용: API 크레딧 없음. 모의 입력만, 메모리 내에서만 작동합니다. Telegram 보고서가 유일한 외부 호출입니다.
아키텍처
[수동 트리거] → [모든 테스트 실행 (코드 노드)] → [보고서 형식화 (코드 노드)] → [보고서 전송 (Telegram)]
로직 표면당 하나의 워크플로우입니다. 필요 시 다시 실행합니다.
표준 테스트 러너 코드 노드
const results = [];
function assert(suite, testName, condition, detail) {
results.push({ suite, test: testName, pass: condition, detail: detail || '' });
}
// ============================================================
// 스위트 1 — <이름>
// ============================================================
// 프로덕션 함수를 여기에 그대로 복사하세요 (가져오기 금지 — 코드 노드는 범위를 공유하지 않음)
function passesJs10Filter(job) {
// ... 프로덕션 노드와 동일한 코드 ...
}
// 테스트
assert('js-10', '보안 요구 직무 거부',
!passesJs10Filter({
title: 'Technical Writer',
description: 'Clearance required.',
location: 'remote'
}),
'보안 요구가 거부를 트리거해야 함');
assert('js-10', '유효한 원격 기술 작가 통과',
passesJs10Filter({
title: 'Technical Writer',
description: 'Write API documentation for cloud platform.',
location: 'remote'
}),
'유효한 원격 기술 작가는 모든 필터를 통과해야 함');
// 아직 구현되지 않은 기능의 경우:
results.push({
suite: 'js-10',
test: '30일 경과 기준 (보류 — 백로그)',
pass: null,
detail: '아직 구현되지 않은 기능입니다. postedAt 날짜 확인을 추가한 후 다시 실행하세요.'
});
// ============================================================
// 요약
// ============================================================
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
}}];
보고서 형식화기
const { summary, results } = $input.first().json;
const passIcon = '✅';
const failIcon = '❌';
const pendIcon = '⏳';
const topEmoji = summary.failed === 0 ? '🟢' : (summary.failed <= 3 ? '🟡' : '🔴');
const lines = [
`<b>${topEmoji} <이름> — 로직 평가</b>`,
`${passIcon} ${summary.passed} 통과 ${failIcon} ${summary.failed} 실패 ${pendIcon} ${summary.pending} 보류`,
''
];
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>실행 시간: ${now} UTC</i>`);
return [{ json: { message: lines.join('\n') } }];
Telegram 전송 노드 (환경 변수 패턴, 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" }
]
}
}
테스트할 내용 (우선순위 순)
- 거부 경로 커버리지 — 레코드가 필터링될 수 있는 모든 이유
- 경계 조건 — 임계값, 바로 아래, 바로 위
- 잘못된 입력에 대한 폴백 —
undefined,null,0,'None', 잘못된 유형 → 예상 기본값 - 알려진 양호한 실제 데이터 — 과거 확인된 실행에서 최소 하나의 예
- 알려진 불량 실제 데이터 — 과거 장애를 일으킨 최소 하나의 예
- 부분 문자열 대 정확한 일치자 — 예:
'HR Dept'vs'Human Resources Department' - 보류 중인 기능 —
pass: null로 작성하여 보고서에 백로그로 표시
테스트 명명 규칙
<스위트>: <사람이 읽을 수 있는 주장> 및 세부 필드에 기본 규칙 또는 과거 버그 참조를 명시:
assert('js-22', 'Human Resources Department 거부',
!isValidContact('Human Resources Department'),
'BAD_CONTAINS_CONTACT 부분 문자열 일치가 이를 잡아야 함 (세션 5 버그 수정)');
"과거 버그를 테스트로 변환" 규칙
교훈 라이브러리의 모든 항목에서 특정 입력 → 잘못된 동작이 명명된 경우, 그 항목은 작성되지 않은 테스트입니다.
실제 교훈 예:
"Aggregate Sector Quotes가 범위=1mo에서 meta.previousClose가 차트 시작 가격을 반환했기 때문에 1일 변동 대신 1개월 변동을 계산하고 있었습니다."
→ 테스트:
assert('aggregate', 'closes[]에서 1일 변동 계산, meta.previousClose 사용 안 함',
computeChange({
chart: { result: [{ meta: { previousClose: 100 }, indicators: { quote: [{ close: [100, 98, 99, 105] }] } }] }
}) === ((105 - 99) / 99) * 100,
'meta.previousClose는 신뢰할 수 없음; closes[-2]를 사용해야 함');
실행 시점
- 필터 로직, 키워드 목록, 검증기, 이력서 맵 변경 전
- 변경 후 — 회귀가 없는지 확인
- 교훈 라이브러리 항목 추가 후 — 해당 항목을 잡았을 테스트 작성
- 새 워크플로우 버전 배포 전
상태 아이콘 의미
| 아이콘 | 의미 |
|---|---|
| 🟢 (녹색) | 실패 0개 — 이 스위트 통과; 다른 검사도 통과하면 배포 가능 |
| 🟡 (노란색) | 1-3개 실패 — 수정될 때까지 배포 차단 |
| 🔴 (빨간색) | 4개 이상 실패 — 배포 차단; 더 광범위한 문제 가능성 |
보류(⏳)는 빨간색/노란색에 포함되지 않습니다 — 별도로 백로그로 추적됩니다. 보류 테스트는 커버리지 격차입니다: 배포 노트에 나열하세요.
안티 패턴
| 안티 패턴 | 대신 할 일 |
|---|---|
| 실시간 API에 대해 테스트 | 모의 입력만 사용 — 평가 스위트는 로직 전용 |
| 보류 중인 기능 건너뛰기 (가시성 상실) | pass: null로 추가하고 누락된 내용을 설명하는 세부 정보 포함 |
| 프로덕션 코드 가져오기 | 그대로 복사 — 코드 노드는 범위를 공유하지 않음 |
| 하나의 거대한 테스트 함수 | 주장당 하나의 assert() — 읽기 쉬운 보고서 |
| 버그 수정 후 테스트 제거 | 영원히 유지 — 회귀 방지 장치임 |
| 행복 경로만 테스트 | 거부 경로 커버리지가 가장 가치 있는 테스트 |
| 변경 후에만 실행 | 변경 전후에 실행 — 회귀를 도입하지 않았는지 확인 |
| 채팅 ID / 토큰을 워크플로우 JSON에 하드코딩 | $env.TELEGRAM_BOT_TOKEN / $env.TELEGRAM_CHAT_ID 사용 |
배포 전 체크리스트
- 평가 워크플로우가 10초 미만으로 실행됨
- 모든 스위트가 보고서에 그룹화됨
- 프로덕션 코드가 그대로 복사됨 (의역 아님)
- 거부 경로당 최소 1개 테스트
- 스위트당 최소 1개 "알려진 양호한 실제 데이터" 테스트
- 보류 중인 기능이
pass: null과 세부 정보로 표현됨 - 보고서가 하나의 Telegram 메시지에 맞음 (< 4000자)
참고
- n8n 코드 노드 문서: https://docs.n8n.io/code/code-node/
- Telegram Bot API 형식: https://core.telegram.org/bots/api#formatting-options