Files
n8n-mcp/docs/testing-architecture.md
2025-07-28 10:21:03 +02:00

13 KiB

n8n-MCP Testing Architecture

Executive Summary

This document outlines a comprehensive testing strategy for the n8n-MCP project, designed to improve from the current 2.45% coverage to a target of 80%+ coverage while ensuring reliability, maintainability, and performance.

Current State Analysis

Problems Identified

  • Low Coverage: 2.45% overall coverage
  • Failing Tests: HTTP server authentication tests failing
  • No CI/CD: No automated testing pipeline
  • Mixed Test Types: Tests scattered without clear organization
  • No Mocking Strategy: Direct dependencies on SQLite, n8n packages
  • No Performance Testing: No benchmarks for critical operations

Testing Framework Strategy

Primary Framework: Vitest (Replacing Jest)

Rationale for Vitest over Jest:

  • Speed: 10-100x faster for large test suites
  • Native ESM Support: Better alignment with modern TypeScript
  • Built-in Mocking: Superior mocking capabilities
  • Watch Mode: Instant feedback during development
  • Compatibility: Jest-compatible API for easy migration
  • Type Safety: Better TypeScript integration

Supporting Frameworks

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    globals: true,
    environment: 'node',
    setupFiles: ['./tests/setup/global-setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/**',
        'dist/**',
        '**/*.d.ts',
        '**/*.test.ts',
        '**/*.spec.ts',
        'tests/**',
        'scripts/**'
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80
      }
    },
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: true
      }
    },
    testTimeout: 30000,
    hookTimeout: 30000
  }
});

Directory Structure

tests/
├── unit/                      # Unit tests (70% of tests)
│   ├── loaders/
│   │   ├── node-loader.test.ts
│   │   └── __mocks__/
│   │       └── n8n-nodes-base.ts
│   ├── parsers/
│   │   ├── node-parser.test.ts
│   │   └── property-extractor.test.ts
│   ├── services/
│   │   ├── property-filter.test.ts
│   │   ├── config-validator.test.ts
│   │   └── workflow-validator.test.ts
│   └── database/
│       ├── node-repository.test.ts
│       └── __mocks__/
│           └── better-sqlite3.ts
├── integration/               # Integration tests (20%)
│   ├── mcp/
│   │   ├── server.test.ts
│   │   └── tools.test.ts
│   ├── n8n-api/
│   │   ├── workflow-crud.test.ts
│   │   └── webhook-trigger.test.ts
│   └── database/
│       ├── sqlite-operations.test.ts
│       └── fts5-search.test.ts
├── e2e/                       # End-to-end tests (10%)
│   ├── workflows/
│   │   ├── complete-workflow.test.ts
│   │   └── ai-agent-workflow.test.ts
│   └── mcp-protocol/
│       └── full-session.test.ts
├── performance/               # Performance benchmarks
│   ├── node-loading.bench.ts
│   ├── search.bench.ts
│   └── validation.bench.ts
├── fixtures/                  # Test data
│   ├── nodes/
│   │   ├── http-request.json
│   │   └── slack.json
│   ├── workflows/
│   │   ├── simple.json
│   │   └── complex-ai.json
│   └── factories/
│       ├── node.factory.ts
│       └── workflow.factory.ts
├── setup/                     # Test configuration
│   ├── global-setup.ts
│   ├── test-containers.ts
│   └── test-database.ts
└── utils/                     # Test utilities
    ├── mocks/
    │   ├── mcp-sdk.ts
    │   └── express.ts
    ├── builders/
    │   ├── node.builder.ts
    │   └── workflow.builder.ts
    └── helpers/
        ├── async.ts
        └── assertions.ts

Mock Strategy

1. Database Mocking

// tests/unit/database/__mocks__/better-sqlite3.ts
import { vi } from 'vitest';

export class Database {
  private data = new Map<string, any[]>();
  
  prepare = vi.fn((sql: string) => ({
    all: vi.fn(() => this.data.get('nodes') || []),
    get: vi.fn((params) => this.data.get('nodes')?.find(n => n.id === params.id)),
    run: vi.fn(),
    finalize: vi.fn()
  }));
  
  exec = vi.fn();
  close = vi.fn();
  
  // Test helper to set mock data
  setMockData(table: string, data: any[]) {
    this.data.set(table, data);
  }
}

export default vi.fn(() => new Database());

2. n8n Package Mocking

// tests/unit/loaders/__mocks__/n8n-nodes-base.ts
import { vi } from 'vitest';
import { readFileSync } from 'fs';
import { join } from 'path';

// Load real node definitions for testing
const mockNodes = JSON.parse(
  readFileSync(join(__dirname, '../../fixtures/nodes/mock-nodes.json'), 'utf8')
);

export const loadClassInIsolation = vi.fn((filePath: string) => {
  const nodeName = filePath.split('/').pop()?.replace('.node.js', '');
  return mockNodes[nodeName] || { description: { properties: [] } };
});

export const NodeHelpers = {
  getVersionedNodeTypeAll: vi.fn(() => [])
};

3. External API Mocking

// tests/utils/mocks/axios.ts
import { vi } from 'vitest';
import type { AxiosRequestConfig } from 'axios';

export const createAxiosMock = () => {
  const mock = {
    get: vi.fn(),
    post: vi.fn(),
    put: vi.fn(),
    delete: vi.fn(),
    create: vi.fn(() => mock)
  };
  
  // Default responses
  mock.get.mockResolvedValue({ data: { success: true } });
  mock.post.mockResolvedValue({ data: { id: '123' } });
  
  return mock;
};

Test Data Management

1. Factory Pattern

// tests/fixtures/factories/node.factory.ts
import { Factory } from 'fishery';
import type { INodeType } from 'n8n-workflow';

export const nodeFactory = Factory.define<INodeType>(({ sequence }) => ({
  name: `TestNode${sequence}`,
  displayName: `Test Node ${sequence}`,
  group: ['test'],
  version: 1,
  description: 'Test node for unit tests',
  defaults: {
    name: `Test Node ${sequence}`,
  },
  inputs: ['main'],
  outputs: ['main'],
  properties: [
    {
      displayName: 'Test Property',
      name: 'testProp',
      type: 'string',
      default: '',
    }
  ]
}));

// Usage in tests:
const httpNode = nodeFactory.build({
  name: 'HttpRequest',
  properties: [/* custom properties */]
});

2. Builder Pattern

// tests/utils/builders/workflow.builder.ts
export class WorkflowBuilder {
  private workflow = {
    name: 'Test Workflow',
    nodes: [],
    connections: {},
    settings: {}
  };
  
  withName(name: string) {
    this.workflow.name = name;
    return this;
  }
  
  addNode(node: any) {
    this.workflow.nodes.push(node);
    return this;
  }
  
  connect(from: string, to: string) {
    this.workflow.connections[from] = {
      main: [[{ node: to, type: 'main', index: 0 }]]
    };
    return this;
  }
  
  build() {
    return this.workflow;
  }
}

CI/CD Pipeline (GitHub Actions)

# .github/workflows/test.yml
name: Test Suite

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x]
        test-suite: [unit, integration, e2e]
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build project
      run: npm run build
    
    - name: Run ${{ matrix.test-suite }} tests
      run: npm run test:${{ matrix.test-suite }}
      env:
        CI: true
    
    - name: Upload coverage
      if: matrix.test-suite == 'unit' && matrix.node-version == '20.x'
      uses: codecov/codecov-action@v4
      with:
        file: ./coverage/lcov.info
        flags: unittests
        name: codecov-umbrella

  performance:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20.x
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run benchmarks
      run: npm run bench
    
    - name: Store benchmark result
      uses: benchmark-action/github-action-benchmark@v1
      with:
        tool: 'vitest'
        output-file-path: bench-results.json
        github-token: ${{ secrets.GITHUB_TOKEN }}
        auto-push: true

  quality:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    
    - name: Install dependencies
      run: npm ci
    
    - name: Lint
      run: npm run lint
    
    - name: Type check
      run: npm run typecheck
    
    - name: Check coverage thresholds
      run: npm run test:coverage:check

Testing Phases and Priorities

Phase 1: Foundation (Weeks 1-2)

  1. Setup Vitest and migrate existing tests
  2. Create mock infrastructure for SQLite and n8n packages
  3. Setup CI/CD pipeline with basic checks
  4. Target: 20% coverage

Phase 2: Core Unit Tests (Weeks 3-4)

  1. Test critical services: validators, parsers, loaders
  2. Database layer with full mocking
  3. MCP tools unit tests
  4. Target: 50% coverage

Phase 3: Integration Tests (Weeks 5-6)

  1. MCP protocol integration tests
  2. n8n API integration with test containers
  3. Database operations with real SQLite
  4. Target: 70% coverage

Phase 4: E2E & Performance (Weeks 7-8)

  1. Complete workflow scenarios
  2. Performance benchmarks for critical paths
  3. Error handling scenarios
  4. Target: 80%+ coverage

Performance Testing

// tests/performance/node-loading.bench.ts
import { bench, describe } from 'vitest';
import { NodeLoader } from '@/loaders/node-loader';

describe('Node Loading Performance', () => {
  bench('Load single node', async () => {
    const loader = new NodeLoader();
    await loader.loadNode('n8n-nodes-base.httpRequest');
  });
  
  bench('Load all nodes', async () => {
    const loader = new NodeLoader();
    await loader.loadAllNodes();
  }, {
    iterations: 10,
    time: 5000 // 5 second time budget
  });
  
  bench('Parse complex node', async () => {
    const parser = new NodeParser();
    await parser.parseNode(complexNodeFixture);
  });
});

Error Testing Strategy

// tests/unit/services/error-scenarios.test.ts
describe('Error Handling', () => {
  it('should handle network failures gracefully', async () => {
    const api = new N8nAPIClient();
    mockAxios.get.mockRejectedValue(new Error('Network error'));
    
    await expect(api.getWorkflow('123'))
      .rejects.toThrow('Failed to fetch workflow');
    
    expect(logger.error).toHaveBeenCalledWith(
      'Network error while fetching workflow',
      expect.any(Error)
    );
  });
  
  it('should handle malformed data', () => {
    const validator = new ConfigValidator();
    const malformed = { nodes: 'not-an-array' };
    
    const result = validator.validate(malformed);
    
    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('nodes must be an array');
  });
});

Coverage Enforcement

// package.json scripts
{
  "scripts": {
    "test": "vitest",
    "test:unit": "vitest run tests/unit",
    "test:integration": "vitest run tests/integration",
    "test:e2e": "vitest run tests/e2e",
    "test:coverage": "vitest run --coverage",
    "test:coverage:check": "vitest run --coverage --coverage.thresholdAutoUpdate=false",
    "test:watch": "vitest watch",
    "bench": "vitest bench",
    "bench:compare": "vitest bench --compare"
  }
}

Monitoring and Reporting

1. Coverage Badges

![Coverage](https://codecov.io/gh/username/n8n-mcp/branch/main/graph/badge.svg)
![Tests](https://github.com/username/n8n-mcp/actions/workflows/test.yml/badge.svg)

2. Performance Tracking

  • Automated benchmark comparisons on PRs
  • Performance regression alerts
  • Historical performance graphs

3. Test Reports

  • HTML coverage reports
  • Failed test summaries in PRs
  • Flaky test detection

Migration Path from Current State

  1. Week 1: Setup Vitest, migrate existing tests
  2. Week 2: Create mock infrastructure
  3. Week 3-4: Write unit tests for critical paths
  4. Week 5-6: Add integration tests
  5. Week 7-8: E2E tests and performance benchmarks

Success Metrics

  • Coverage: 80%+ overall, 90%+ for critical paths
  • Performance: All operations under 100ms
  • Reliability: Zero flaky tests
  • CI Time: Full suite under 5 minutes
  • Developer Experience: Tests run in <1 second locally