mcpField
Q
qe-n8n-integration-testing-patterns

qe-n8n-integration-testing-patterns

github.com/proffesor-for-testingテスト・品質51980 回インストール今日更新
リポジトリ

qe-n8n-integration-testing-patterns について

n8n の連携をテストするためのスキルで、開発者による認証情報、操作、API 動作の検証を支援します。接続、認証、レート制限、レスポンス処理、エラーを確認します。

主な機能

  1. 01接続と認証情報の確認
  2. 02OAuth2 と API キーの検証
  3. 03設定済み操作のテスト
  4. 04レート制限動作の確認
  5. 05API レスポンスとエラーの確認

活用シーン

  1. 01ワークフロー利用前の連携検証
  2. 02OAuth2 トークン更新の確認
  3. 03レート制限下での API 動作テスト

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/qe-n8n-integration-testing-patternsを入力して読み込まれたか確認します。

詳細

n8n 統合テストのパターン

<default_to_action> n8n 統合をテストする際は、次の手順に従います。

  1. 接続と認証を検証する
  2. 設定済みのすべての操作をテストする
  3. API レスポンスの処理を検証する
  4. レート制限への対応を確認する
  5. エラー処理が機能することを確認する

統合のクイックチェックリスト:

  • 認証情報が有効で、期限切れになっていない
  • API 権限が操作に対して十分である
  • レート制限を把握し、遵守している
  • エラーレスポンスを適切に処理している
  • データ形式が API の想定と一致している

重要な成功要因:

  • ワークフローに統合する前に、単独でテストする
  • OAuth トークンの更新が機能することを検証する
  • API バージョンとの互換性を確認する
  • レート制限ヘッダーを監視する </default_to_action>

クイックリファレンスカード

n8n の一般的な統合
カテゴリーサービス認証タイプ
コミュニケーションSlack、Teams、DiscordOAuth2、Webhook
データストレージGoogle Sheets、AirtableOAuth2、API Key
CRMSalesforce、HubSpotOAuth2
開発ツールGitHub、Jira、LinearOAuth2、API Key
マーケティングMailchimp、SendGridAPI Key
認証タイプ
タイプ設定更新
OAuth2ユーザー認証フロートークンを自動更新
API Keyキーを手動で入力手動でローテーション
Basic Authユーザー名/パスワード更新不要
Header Authカスタムヘッダー手動でローテーション

接続テスト

// Test integration connectivity
async function testIntegrationConnectivity(nodeName: string): Promise<ConnectivityResult> {
  const node = await getNodeConfig(nodeName);

  // Check credential exists
  if (!node.credentials) {
    return { connected: false, error: 'No credentials configured' };
  }

  // Test based on integration type
  switch (getIntegrationType(node.type)) {
    case 'slack':
      return await testSlackConnectivity(node.credentials);
    case 'google-sheets':
      return await testGoogleSheetsConnectivity(node.credentials);
    case 'jira':
      return await testJiraConnectivity(node.credentials);
    case 'github':
      return await testGitHubConnectivity(node.credentials);
    default:
      return await testGenericAPIConnectivity(node);
  }
}

// Slack connectivity test
async function testSlackConnectivity(credentials: any): Promise<ConnectivityResult> {
  try {
    const response = await fetch('https://slack.com/api/auth.test', {
      headers: { 'Authorization': `Bearer ${credentials.accessToken}` }
    });
    const data = await response.json();

    return {
      connected: data.ok,
      workspace: data.team,
      user: data.user,
      scopes: data.response_metadata?.scopes || []
    };
  } catch (error) {
    return { connected: false, error: error.message };
  }
}

// Google Sheets connectivity test
async function testGoogleSheetsConnectivity(credentials: any): Promise<ConnectivityResult> {
  try {
    const response = await fetch('https://www.googleapis.com/drive/v3/about?fields=user', {
      headers: { 'Authorization': `Bearer ${credentials.accessToken}` }
    });

    if (response.status === 401) {
      // Try refresh
      const refreshed = await refreshOAuthToken(credentials);
      if (refreshed) {
        return testGoogleSheetsConnectivity({ ...credentials, accessToken: refreshed });
      }
      return { connected: false, error: 'Token expired, refresh failed' };
    }

    const data = await response.json();
    return { connected: true, user: data.user };
  } catch (error) {
    return { connected: false, error: error.message };
  }
}

API 操作テスト

// Test integration operations
async function testIntegrationOperations(nodeName: string): Promise<OperationResult[]> {
  const node = await getNodeConfig(nodeName);
  const operations = getNodeOperations(node.type);
  const results: OperationResult[] = [];

  for (const operation of operations) {
    const testData = generateTestData(node.type, operation);

    try {
      const startTime = Date.now();
      const response = await executeOperation(node, operation, testData);

      results.push({
        operation,
        success: true,
        responseTime: Date.now() - startTime,
        responseStatus: response.status,
        dataValid: validateResponseData(response.data, operation)
      });
    } catch (error) {
      results.push({
        operation,
        success: false,
        error: error.message,
        errorType: classifyError(error)
      });
    }
  }

  return results;
}

// Generate test data for operations
function generateTestData(nodeType: string, operation: string): any {
  const testDataMap = {
    'slack': {
      'postMessage': {
        channel: 'C123456',
        text: 'Test message from n8n integration test'
      },
      'uploadFile': {
        channels: 'C123456',
        content: 'Test file content',
        filename: 'test.txt'
      }
    },
    'google-sheets': {
      'appendData': {
        spreadsheetId: 'test-spreadsheet-id',
        range: 'Sheet1!A:Z',
        values: [['Test', 'Data', new Date().toISOString()]]
      },
      'readRows': {
        spreadsheetId: 'test-spreadsheet-id',
        range: 'Sheet1!A1:Z10'
      }
    },
    'jira': {
      'createIssue': {
        project: 'TEST',
        issueType: 'Task',
        summary: 'Test issue from n8n',
        description: 'Created by integration test'
      },
      'updateIssue': {
        issueKey: 'TEST-1',
        fields: { summary: 'Updated by n8n test' }
      }
    }
  };

  return testDataMap[nodeType]?.[operation] || {};
}

認証テスト

OAuth2 フローのテスト
// Test OAuth2 authentication
async function testOAuth2Authentication(credentials: any): Promise<OAuth2Result> {
  const result: OAuth2Result = {
    tokenValid: false,
    refreshWorking: false,
    scopes: [],
    expiresIn: 0
  };

  // Test current token
  const tokenTest = await testAccessToken(credentials.accessToken);
  result.tokenValid = tokenTest.valid;
  result.scopes = tokenTest.scopes;

  // Check expiration
  if (credentials.expiresAt) {
    result.expiresIn = new Date(credentials.expiresAt).getTime() - Date.now();
    result.expiresSoon = result.expiresIn < 3600000; // Less than 1 hour
  }

  // Test refresh token
  if (credentials.refreshToken) {
    try {
      const newToken = await refreshOAuthToken(credentials);
      result.refreshWorking = !!newToken;
    } catch (error) {
      result.refreshError = error.message;
    }
  }

  return result;
}

// Test required scopes
async function testRequiredScopes(credentials: any, requiredScopes: string[]): Promise<ScopeResult> {
  const currentScopes = await getTokenScopes(credentials.accessToken);
  const missingScopes = requiredScopes.filter(s => !currentScopes.includes(s));

  return {
    hasAllScopes: missingScopes.length === 0,
    currentScopes,
    missingScopes,
    recommendation: missingScopes.length > 0
      ? `Re-authorize with scopes: ${missingScopes.join(', ')}`
      : null
  };
}
API Key のテスト
// Test API key validity
async function testAPIKey(integration: string, apiKey: string): Promise<APIKeyResult> {
  const endpoints = {
    'sendgrid': 'https://api.sendgrid.com/v3/user/profile',
    'mailchimp': 'https://us1.api.mailchimp.com/3.0/ping',
    'airtable': 'https://api.airtable.com/v0/meta/whoami'
  };

  const endpoint = endpoints[integration];
  if (!endpoint) {
    return { valid: false, error: 'Unknown integration' };
  }

  try {
    const response = await fetch(endpoint, {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });

    return {
      valid: response.status === 200,
      status: response.status,
      rateLimit: extractRateLimitInfo(response.headers)
    };
  } catch (error) {
    return { valid: false, error: error.message };
  }
}

レート制限テスト

// Test rate limit handling
async function testRateLimits(nodeName: string, requestCount: number): Promise<RateLimitResult> {
  const results: RequestResult[] = [];
  let rateLimitHit = false;
  let retryAfter = 0;

  for (let i = 0; i < requestCount; i++) {
    const startTime = Date.now();
    const response = await makeRequest(nodeName);

    results.push({
      requestNumber: i + 1,
      status: response.status,
      responseTime: Date.now() - startTime,
      rateLimitRemaining: response.headers['x-ratelimit-remaining'],
      rateLimitLimit: response.headers['x-ratelimit-limit']
    });

    if (response.status === 429) {
      rateLimitHit = true;
      retryAfter = parseInt(response.headers['retry-after'] || '60');
      break;
    }

    // Small delay between requests
    await sleep(100);
  }

  return {
    requestsMade: results.length,
    rateLimitHit,
    retryAfter,
    results,
    recommendation: rateLimitHit
      ? `Implement exponential backoff, retry after ${retryAfter}s`
      : 'Rate limit not reached, consider increasing request count for thorough testing'
  };
}

// Extract rate limit info from headers
function extractRateLimitInfo(headers: Headers): RateLimitInfo {
  return {
    limit: headers.get('x-ratelimit-limit'),
    remaining: headers.get('x-ratelimit-remaining'),
    reset: headers.get('x-ratelimit-reset'),
    retryAfter: headers.get('retry-after')
  };
}

エラー処理テスト

// Test error scenarios
async function testErrorScenarios(nodeName: string): Promise<ErrorTestResult[]> {
  const scenarios = [
    { name: 'Invalid credentials', modify: { credentials: null } },
    { name: 'Invalid endpoint', modify: { url: 'https://invalid.example.com' } },
    { name: 'Timeout', modify: { timeout: 1 } },
    { name: 'Invalid data', modify: { data: { invalid: true } } },
    { name: 'Not found', modify: { resourceId: 'nonexistent-123' } },
    { name: 'Permission denied', modify: { scope: 'read-only' } }
  ];

  const results: ErrorTestResult[] = [];

  for (const scenario of scenarios) {
    try {
      const response = await executeWithModification(nodeName, scenario.modify);

      results.push({
        scenario: scenario.name,
        errorHandled: response.status >= 400,
        errorCode: response.status,
        errorMessage: response.data?.error?.message,
        retried: response.metadata?.retryCount > 0
      });
    } catch (error) {
      results.push({
        scenario: scenario.name,
        errorHandled: true,
        exceptionThrown: true,
        errorType: error.constructor.name,
        errorMessage: error.message
      });
    }
  }

  return results;
}

// Classify error types
function classifyError(error: any): string {
  if (error.status === 401 || error.status === 403) return 'authentication';
  if (error.status === 404) return 'not-found';
  if (error.status === 429) return 'rate-limit';
  if (error.status >= 500) return 'server-error';
  if (error.code === 'ETIMEDOUT') return 'timeout';
  if (error.code === 'ECONNREFUSED') return 'connection';
  return 'unknown';
}

統合固有のパターン

Slack 統合
const slackTestPatterns = {
  // Test message posting
  testPostMessage: async (credentials) => {
    return await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${credentials.accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        channel: 'C123456',
        text: 'Integration test message'
      })
    });
  },

  // Test file upload
  testFileUpload: async (credentials) => {
    const formData = new FormData();
    formData.append('channels', 'C123456');
    formData.append('content', 'Test file content');
    formData.append('filename', 'test.txt');

    return await fetch('https://slack.com/api/files.upload', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${credentials.accessToken}` },
      body: formData
    });
  },

  // Validate required scopes
  requiredScopes: ['chat:write', 'files:write', 'channels:read']
};
Google Sheets 統合
const googleSheetsTestPatterns = {
  // Test read operation
  testReadRows: async (credentials, spreadsheetId) => {
    return await fetch(
      `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/Sheet1!A1:Z10`,
      { headers: { 'Authorization': `Bearer ${credentials.accessToken}` } }
    );
  },

  // Test append operation
  testAppendRow: async (credentials, spreadsheetId, values) => {
    return await fetch(
      `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/Sheet1!A:Z:append?valueInputOption=USER_ENTERED`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${credentials.accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ values: [values] })
      }
    );
  },

  // Required scopes
  requiredScopes: ['https://www.googleapis.com/auth/spreadsheets']
};

テストレポートのテンプレート

# Integration Test Report

## Summary
| Integration | Status | Auth | Operations | Errors |
|-------------|--------|------|------------|--------|
| Slack | PASS | OK | 4/4 | 0 |
| Google Sheets | WARN | Expiring | 3/3 | 0 |
| Jira | FAIL | OK | 2/4 | 2 |

## Authentication Status
- Slack: OAuth2 valid, expires in 28 days
- Google Sheets: OAuth2 expires in 2 hours - REFRESH RECOMMENDED
- Jira: API Key valid

## Rate Limit Status
| Integration | Limit | Used | Remaining |
|-------------|-------|------|-----------|
| Slack | 50/min | 12 | 38 |
| Google Sheets | 100/min | 45 | 55 |
| Jira | 100/min | 8 | 92 |

## Failed Operations
### Jira: Transition Issue
- Error: Invalid transition for current state
- Fix: Check workflow transitions in Jira

## Recommendations
1. Refresh Google Sheets OAuth token before expiration
2. Fix Jira workflow transition logic

関連スキル


覚えておきましょう

n8n は 400 以上のサービスと統合でき、それぞれに固有の認証、レート制限、API の仕様があります。テストでは次のことが必要です。

  • 接続の検証
  • 認証の検証(OAuth の更新、API キーの有効期限)
  • 現実的なデータを使った操作テスト
  • レート制限の把握
  • エラー処理の検証

エージェントを使う場合: 包括的な統合テストには n8n-integration-test を使用します。統合を実際の文脈でテストするために、n8n-workflow-executor と連携してください。

コメント