mcpField
Q
qe-n8n-trigger-testing-strategies

qe-n8n-trigger-testing-strategies

github.com/proffesor-for-testing测试与质量51984 次安装今天更新
代码仓库

关于 qe-n8n-trigger-testing-strategies

面向 n8n 工作流测试者的技能,提供针对 Webhook、计划、轮询和事件触发器的检查要点与测试示例。内容覆盖输入、认证、错误响应及可靠性。

主要功能

  1. 01区分四类触发器
  2. 02检查 Webhook 载荷与 HTTP 方法
  3. 03验证认证和授权
  4. 04检查计划与轮询可靠性
  5. 05测试事件处理和响应模式

适用场景

  1. 01验证新 Webhook 的输入和认证
  2. 02检查计划触发的 Cron 与时区行为
  3. 03排查轮询重复或遗漏触发
  4. 04评估触发器超时和响应表现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/proffesor-for-testing/agentic-qe --skill qe-n8n-trigger-testing-strategies

安装后在 Claude Code 中输入/qe-n8n-trigger-testing-strategies确认已加载。

详细介绍

n8n 触发器测试策略

<default_to_action> 测试 n8n 触发器时:

  1. 确定触发器类型(webhook、计划、轮询、事件)
  2. 使用各种有效负载进行测试
  3. 验证身份验证和授权
  4. 检查对无效输入的错误处理
  5. 测量响应时间和可靠性

快速触发器检查清单:

  • 触发器正确激活工作流
  • 解析并验证有效负载
  • 强制执行身份验证(如已配置)
  • 错误响应信息明确
  • 响应时间可接受

关键成功因素:

  • 测试边界情况(空有效负载、大型有效负载)
  • 在需要时验证幂等性
  • 检查超时处理
  • 监控是否有触发器遗漏 </default_to_action>

快速参考卡

n8n 触发器类型
类型用例测试重点
Webhook外部 HTTP 调用有效负载、身份验证、方法
计划定时执行Cron 准确性、时区
轮询检查更改间隔、去重
事件服务事件事件处理、筛选
常见 Webhook 配置
设置选项影响
HTTP 方法GET、POST、PUT、DELETE请求处理
身份验证无、Basic、Header安全性
响应模式立即、最后一个节点、自定义响应时机
路径自定义 URL 路径端点标识

Webhook 测试

基本 Webhook 测试
// 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 方法测试
// 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;
}
身份验证测试
// 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
  };
}

计划测试

Cron 表达式验证
// 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;
}
计划可靠性测试
// 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
  };
}

轮询触发器测试

// 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;
}

事件触发器测试

// 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
  };
}

触发器响应测试

// 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'
  };
}

测试场景

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

相关技能


请记住

n8n 触发器是工作流的入口点。测试需要关注:

  • Webhook:有效负载处理、身份验证、HTTP 方法
  • 计划:Cron 准确性、时区处理
  • 轮询:间隔准确性、去重
  • 事件:事件处理、筛选

关键模式: 使用各种有效负载(有效、无效、边界情况)进行测试。验证身份验证是否得到强制执行。检查响应时间和长期可靠性。

评论

支持 Markdown