Agentic-qe n8n-trigger-testing-strategies
Webhook testing, schedule validation, event-driven triggers, and polling mechanism testing for n8n workflows. Use when testing how workflows are triggered.
install
source · Clone the upstream repo
git clone https://github.com/proffesor-for-testing/agentic-qe
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/proffesor-for-testing/agentic-qe "$T" && mkdir -p ~/.claude/skills && cp -r "$T/assets/skills/n8n-trigger-testing-strategies" ~/.claude/skills/proffesor-for-testing-agentic-qe-n8n-trigger-testing-strategies-335b22 && rm -rf "$T"
manifest:
assets/skills/n8n-trigger-testing-strategies/SKILL.mdsource content
n8n Trigger Testing Strategies
<default_to_action> When testing n8n triggers:
- IDENTIFY trigger type (webhook, schedule, polling, event)
- TEST with various valid payloads
- VERIFY authentication and authorization
- CHECK error handling for invalid inputs
- MEASURE response time and reliability
Quick Trigger Checklist:
- Trigger activates workflow correctly
- Payload parsed and validated
- Authentication enforced (if configured)
- Error responses are informative
- Response time is acceptable
Critical Success Factors:
- Test edge cases (empty payloads, large payloads)
- Verify idempotency where needed
- Check timeout handling
- Monitor for missed triggers </default_to_action>
Quick Reference Card
n8n Trigger Types
| Type | Use Case | Testing Focus |
|---|---|---|
| Webhook | External HTTP calls | Payloads, auth, methods |
| Schedule | Timed execution | Cron accuracy, timezone |
| Polling | Check for changes | Interval, deduplication |
| Event | Service events | Event handling, filtering |
Common Webhook Configurations
| Setting | Options | Impact |
|---|---|---|
| HTTP Method | GET, POST, PUT, DELETE | Request handling |
| Authentication | None, Basic, Header | Security |
| Response Mode | Immediately, Last Node, Custom | Response timing |
| Path | Custom URL path | Endpoint identification |
Webhook Testing
Basic Webhook Test
// Test webhook with various payloads async function testWebhook(webhookUrl: string): Promise<WebhookTestResult> { const testPayloads = [ // Valid JSON { type: 'json', data: { event: 'test', timestamp: Date.now() } }, // Empty object { type: 'empty', data: {} }, // Large payload { type: 'large', data: { items: Array(1000).fill({ id: 1, name: 'test' }) } }, // Nested data { type: 'nested', data: { level1: { level2: { level3: { value: 'deep' } } } } }, // Special characters { type: 'special', data: { text: 'Hello <script>alert("xss")</script>' } } ]; const results: PayloadTestResult[] = []; for (const payload of testPayloads) { const startTime = Date.now(); try { const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload.data) }); results.push({ payloadType: payload.type, success: response.ok, status: response.status, responseTime: Date.now() - startTime, responseBody: await response.text() }); } catch (error) { results.push({ payloadType: payload.type, success: false, error: error.message }); } } return { webhookUrl, results }; }
HTTP Method Testing
// Test all HTTP methods async function testWebhookMethods(webhookUrl: string): Promise<MethodTestResult[]> { const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; const results: MethodTestResult[] = []; for (const method of methods) { try { const response = await fetch(webhookUrl, { method, headers: { 'Content-Type': 'application/json' }, body: ['GET', 'HEAD', 'OPTIONS'].includes(method) ? undefined : '{}' }); results.push({ method, allowed: response.ok || response.status !== 405, status: response.status, statusText: response.statusText }); } catch (error) { results.push({ method, allowed: false, error: error.message }); } } return results; }
Authentication Testing
// Test webhook authentication async function testWebhookAuth(webhookUrl: string, authConfig: AuthConfig): Promise<AuthTestResult> { const scenarios = [ // No auth { name: 'no-auth', headers: {} }, // Invalid auth { name: 'invalid-auth', headers: { 'Authorization': 'Bearer invalid-token' } }, // Valid auth { name: 'valid-auth', headers: { 'Authorization': `Bearer ${authConfig.token}` } }, // Expired auth { name: 'expired-auth', headers: { 'Authorization': `Bearer ${authConfig.expiredToken}` } } ]; const results: AuthScenarioResult[] = []; for (const scenario of scenarios) { const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...scenario.headers }, body: '{}' }); results.push({ scenario: scenario.name, status: response.status, authenticated: response.ok, errorMessage: response.ok ? null : await response.text() }); } return { authRequired: !results.find(r => r.scenario === 'no-auth')?.authenticated, invalidRejected: !results.find(r => r.scenario === 'invalid-auth')?.authenticated, validAccepted: results.find(r => r.scenario === 'valid-auth')?.authenticated, results }; }
Schedule Testing
Cron Expression Validation
// Validate cron expression function validateCronExpression(expression: string): CronValidationResult { const parts = expression.trim().split(/\s+/); if (parts.length < 5 || parts.length > 6) { return { valid: false, error: `Expected 5-6 parts, got ${parts.length}` }; } const [minute, hour, dayOfMonth, month, dayOfWeek, year] = parts; const validations = [ { field: 'minute', value: minute, range: [0, 59] }, { field: 'hour', value: hour, range: [0, 23] }, { field: 'dayOfMonth', value: dayOfMonth, range: [1, 31] }, { field: 'month', value: month, range: [1, 12] }, { field: 'dayOfWeek', value: dayOfWeek, range: [0, 7] } ]; for (const v of validations) { const result = validateCronField(v.value, v.range); if (!result.valid) { return { valid: false, error: `Invalid ${v.field}: ${result.error}` }; } } return { valid: true, description: describeCronExpression(expression), nextExecutions: getNextCronExecutions(expression, 5) }; } // Get human-readable description function describeCronExpression(expression: string): string { // Common patterns const patterns: Record<string, string> = { '* * * * *': 'Every minute', '*/5 * * * *': 'Every 5 minutes', '0 * * * *': 'Every hour', '0 0 * * *': 'Every day at midnight', '0 9 * * 1-5': 'Weekdays at 9:00 AM', '0 0 1 * *': 'First day of every month', '0 0 * * 0': 'Every Sunday at midnight' }; return patterns[expression] || 'Custom schedule'; } // Calculate next execution times function getNextCronExecutions(expression: string, count: number): Date[] { const executions: Date[] = []; let current = new Date(); // Simple implementation - use cron-parser library in production for (let i = 0; i < count; i++) { const next = calculateNextCronExecution(expression, current); executions.push(next); current = new Date(next.getTime() + 60000); // Move past this execution } return executions; }
Schedule Reliability Testing
// Test schedule trigger reliability async function testScheduleReliability(triggerId: string, testDuration: number): Promise<ScheduleTestResult> { const startTime = Date.now(); const expectedExecutions: Date[] = []; const actualExecutions: Date[] = []; // Calculate expected execution times const cronExpression = await getTriggerCronExpression(triggerId); let checkTime = new Date(startTime); while (checkTime.getTime() < startTime + testDuration) { const nextExec = calculateNextCronExecution(cronExpression, checkTime); if (nextExec.getTime() < startTime + testDuration) { expectedExecutions.push(nextExec); } checkTime = new Date(nextExec.getTime() + 60000); } // Monitor actual executions const executionListener = onExecutionStart(triggerId, (exec) => { actualExecutions.push(new Date(exec.startedAt)); }); // Wait for test duration await sleep(testDuration); executionListener.stop(); // Compare expected vs actual const comparison = compareExecutions(expectedExecutions, actualExecutions); return { testDuration, expectedCount: expectedExecutions.length, actualCount: actualExecutions.length, missedExecutions: comparison.missed, extraExecutions: comparison.extra, timingAccuracy: comparison.timingAccuracy, reliability: (actualExecutions.length / expectedExecutions.length) * 100 }; }
Polling Trigger Testing
// Test polling trigger behavior async function testPollingTrigger(triggerId: string, testConfig: PollingTestConfig): Promise<PollingTestResult> { const { interval, testDuration, simulateDataChanges } = testConfig; const pollEvents: PollEvent[] = []; const triggeredExecutions: Execution[] = []; // Monitor polling events const pollListener = onPoll(triggerId, (event) => { pollEvents.push({ timestamp: new Date(), dataFound: event.hasNewData, itemCount: event.items?.length || 0 }); }); // Monitor triggered executions const execListener = onExecutionStart(triggerId, (exec) => { triggeredExecutions.push(exec); }); // Optionally simulate data changes if (simulateDataChanges) { for (const change of simulateDataChanges) { setTimeout(() => { injectTestData(triggerId, change.data); }, change.at); } } // Wait for test duration await sleep(testDuration); pollListener.stop(); execListener.stop(); // Analyze results const expectedPolls = Math.floor(testDuration / interval); const actualPolls = pollEvents.length; return { interval, testDuration, expectedPolls, actualPolls, pollAccuracy: (actualPolls / expectedPolls) * 100, averageInterval: calculateAverageInterval(pollEvents), executionsTriggered: triggeredExecutions.length, deduplicationWorking: checkDeduplication(triggeredExecutions), pollEvents }; } // Check if deduplication is working function checkDeduplication(executions: Execution[]): boolean { const processedIds = new Set(); for (const exec of executions) { const itemIds = exec.data?.resultData?.runData?.Trigger?.[0]?.data?.main?.[0] ?.map(item => item.json?.id); if (itemIds) { for (const id of itemIds) { if (processedIds.has(id)) { return false; // Duplicate found } processedIds.add(id); } } } return true; }
Event Trigger Testing
// Test event-driven triggers async function testEventTrigger(triggerId: string, eventConfig: EventTestConfig): Promise<EventTestResult> { const { eventType, testEvents, timeout } = eventConfig; const results: EventResult[] = []; for (const testEvent of testEvents) { // Emit test event const startTime = Date.now(); await emitTestEvent(eventType, testEvent.payload); // Wait for trigger try { const execution = await waitForTrigger(triggerId, timeout); results.push({ eventType: testEvent.type, triggered: true, latency: Date.now() - startTime, payloadReceived: execution.data?.inputData }); } catch (error) { results.push({ eventType: testEvent.type, triggered: false, error: error.message }); } } return { eventType, testsRun: testEvents.length, triggered: results.filter(r => r.triggered).length, averageLatency: average(results.filter(r => r.triggered).map(r => r.latency)), results }; }
Trigger Response Testing
// Test trigger response modes async function testTriggerResponses(webhookUrl: string): Promise<ResponseTestResult> { // Test immediate response const immediateStart = Date.now(); const immediateResponse = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"test": "immediate"}' }); const immediateTime = Date.now() - immediateStart; // Test with workflow execution const workflowStart = Date.now(); const workflowResponse = await fetch(`${webhookUrl}?waitForResponse=true`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"test": "workflow"}' }); const workflowTime = Date.now() - workflowStart; return { immediateResponse: { status: immediateResponse.status, time: immediateTime, body: await immediateResponse.text() }, workflowResponse: { status: workflowResponse.status, time: workflowTime, body: await workflowResponse.text() }, responseMode: workflowTime > immediateTime + 100 ? 'workflow' : 'immediate' }; }
Test Scenarios
Webhook Scenarios: - name: Valid JSON POST method: POST payload: {"event": "test"} expected: 200 OK - name: Invalid JSON method: POST payload: "not valid json" expected: 400 Bad Request - name: Missing auth method: POST headers: {} expected: 401 Unauthorized - name: Large payload method: POST payload: [10MB of data] expected: 413 Payload Too Large Schedule Scenarios: - name: Every 5 minutes cron: "*/5 * * * *" verify: 12 executions per hour - name: Weekdays at 9 AM cron: "0 9 * * 1-5" verify: 5 executions per week Polling Scenarios: - name: 1 minute interval interval: 60000 verify: ~60 polls per hour - name: Deduplication interval: 60000 inject_duplicate: true verify: No duplicate processing
Related Skills
Remember
n8n triggers are the entry points to workflows. Testing requires:
- Webhook: Payload handling, auth, HTTP methods
- Schedule: Cron accuracy, timezone handling
- Polling: Interval accuracy, deduplication
- Event: Event handling, filtering
Key patterns: Test with various payloads (valid, invalid, edge cases). Verify authentication enforcement. Check response times and reliability over time.