Files
n8n-mcp/tests/MOCKING_STRATEGY.md
czlonkowski b49043171e test: Phase 3 - Create comprehensive unit tests for services
- Add unit tests for ConfigValidator with 44 test cases (95.21% coverage)
- Create test templates for 7 major services:
  - PropertyFilter (23 tests)
  - ExampleGenerator (35 tests)
  - TaskTemplates (36 tests)
  - PropertyDependencies (21 tests)
  - EnhancedConfigValidator (8 tests)
  - ExpressionValidator (11 tests)
  - WorkflowValidator (9 tests)
- Fix service implementations to handle edge cases discovered during testing
- Add comprehensive testing documentation:
  - Phase 3 testing plan with priorities and timeline
  - Context documentation for quick implementation
  - Mocking strategy for complex dependencies
- All 262 tests now passing (up from 75)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-28 14:15:09 +02:00

8.1 KiB

Mocking Strategy for n8n-mcp Services

Overview

This document outlines the mocking strategy for testing services with complex dependencies. The goal is to achieve reliable tests without over-mocking.

Service Dependency Map

graph TD
    CV[ConfigValidator] --> NSV[NodeSpecificValidators]
    ECV[EnhancedConfigValidator] --> CV
    ECV --> NSV
    WV[WorkflowValidator] --> NR[NodeRepository]
    WV --> ECV
    WV --> EV[ExpressionValidator]
    WDE[WorkflowDiffEngine] --> NV[n8n-validation]
    NAC[N8nApiClient] --> AX[axios]
    NAC --> NV
    NDS[NodeDocumentationService] --> NR
    PD[PropertyDependencies] --> NR

Mocking Guidelines

1. Database Layer (NodeRepository)

When to Mock: Always mock database access in unit tests

// Mock Setup
vi.mock('@/database/node-repository', () => ({
  NodeRepository: vi.fn().mockImplementation(() => ({
    getNode: vi.fn().mockImplementation((nodeType: string) => {
      // Return test fixtures based on nodeType
      const fixtures = {
        'nodes-base.httpRequest': httpRequestNodeFixture,
        'nodes-base.slack': slackNodeFixture,
        'nodes-base.webhook': webhookNodeFixture
      };
      return fixtures[nodeType] || null;
    }),
    searchNodes: vi.fn().mockReturnValue([]),
    listNodes: vi.fn().mockReturnValue([])
  }))
}));

2. HTTP Client (axios)

When to Mock: Always mock external HTTP calls

// Mock Setup
vi.mock('axios');

beforeEach(() => {
  const mockAxiosInstance = {
    get: vi.fn().mockResolvedValue({ data: {} }),
    post: vi.fn().mockResolvedValue({ data: {} }),
    put: vi.fn().mockResolvedValue({ data: {} }),
    delete: vi.fn().mockResolvedValue({ data: {} }),
    patch: vi.fn().mockResolvedValue({ data: {} }),
    interceptors: {
      request: { use: vi.fn() },
      response: { use: vi.fn() }
    },
    defaults: { baseURL: 'http://test.n8n.local/api/v1' }
  };
  
  (axios.create as any).mockReturnValue(mockAxiosInstance);
});

3. Service-to-Service Dependencies

Strategy: Mock at service boundaries, not internal methods

// Good: Mock the imported service
vi.mock('@/services/node-specific-validators', () => ({
  NodeSpecificValidators: {
    validateSlack: vi.fn(),
    validateHttpRequest: vi.fn(),
    validateCode: vi.fn()
  }
}));

// Bad: Don't mock internal methods
// validator.checkRequiredProperties = vi.fn(); // DON'T DO THIS

4. Complex Objects (Workflows, Nodes)

Strategy: Use factories and fixtures, not inline mocks

// Good: Use factory
import { workflowFactory } from '@tests/fixtures/factories/workflow.factory';
const workflow = workflowFactory.withConnections();

// Bad: Don't create complex objects inline
const workflow = { nodes: [...], connections: {...} }; // Avoid

Service-Specific Mocking Strategies

ConfigValidator & EnhancedConfigValidator

Dependencies: NodeSpecificValidators (circular)

Strategy:

  • Test base validation logic without mocking
  • Mock NodeSpecificValidators only when testing integration points
  • Use real property definitions from fixtures
// Test pure validation logic without mocks
it('validates required properties', () => {
  const properties = [
    { name: 'url', type: 'string', required: true }
  ];
  const result = ConfigValidator.validate('nodes-base.httpRequest', {}, properties);
  expect(result.errors).toContainEqual(
    expect.objectContaining({ type: 'missing_required' })
  );
});

WorkflowValidator

Dependencies: NodeRepository, EnhancedConfigValidator, ExpressionValidator

Strategy:

  • Mock NodeRepository with comprehensive fixtures
  • Use real EnhancedConfigValidator for integration testing
  • Mock only for isolated unit tests
const mockNodeRepo = {
  getNode: vi.fn().mockImplementation((type) => {
    // Return node definitions with typeVersion info
    return nodesDatabase[type] || null;
  })
};

const validator = new WorkflowValidator(
  mockNodeRepo as any,
  EnhancedConfigValidator // Use real validator
);

N8nApiClient

Dependencies: axios, n8n-validation

Strategy:

  • Mock axios completely
  • Use real n8n-validation functions
  • Test each endpoint with success/error scenarios
describe('workflow operations', () => {
  it('handles PUT fallback to PATCH', async () => {
    mockAxios.put.mockRejectedValueOnce({ 
      response: { status: 405 } 
    });
    mockAxios.patch.mockResolvedValueOnce({ 
      data: workflowFixture 
    });
    
    const result = await client.updateWorkflow('123', workflow);
    expect(mockAxios.patch).toHaveBeenCalled();
  });
});

WorkflowDiffEngine

Dependencies: n8n-validation

Strategy:

  • Use real validation functions
  • Create comprehensive workflow fixtures
  • Test state transitions with snapshots
it('applies node operations in correct order', async () => {
  const workflow = workflowFactory.minimal();
  const operations = [
    { type: 'addNode', node: nodeFactory.httpRequest() },
    { type: 'addConnection', source: 'trigger', target: 'HTTP Request' }
  ];
  
  const result = await engine.applyDiff(workflow, { operations });
  expect(result.workflow).toMatchSnapshot();
});

ExpressionValidator

Dependencies: None (pure functions)

Strategy:

  • No mocking needed
  • Test with comprehensive expression fixtures
  • Focus on edge cases and error scenarios
const expressionFixtures = {
  valid: [
    '{{ $json.field }}',
    '{{ $node["HTTP Request"].json.data }}',
    '{{ $items("Split In Batches", 0) }}'
  ],
  invalid: [
    '{{ $json[notANumber] }}',
    '{{ ${template} }}', // Template literals
    '{{ json.field }}' // Missing $
  ]
};

Test Data Management

1. Fixture Organization

tests/fixtures/
├── nodes/
│   ├── http-request.json
│   ├── slack.json
│   └── webhook.json
├── workflows/
│   ├── minimal.json
│   ├── with-errors.json
│   └── ai-agent.json
├── expressions/
│   ├── valid.json
│   └── invalid.json
└── factories/
    ├── node.factory.ts
    ├── workflow.factory.ts
    └── validation.factory.ts

2. Fixture Loading

// Helper to load JSON fixtures
export const loadFixture = (path: string) => {
  return JSON.parse(
    fs.readFileSync(
      path.join(__dirname, '../fixtures', path), 
      'utf-8'
    )
  );
};

// Usage
const slackNode = loadFixture('nodes/slack.json');

Anti-Patterns to Avoid

1. Over-Mocking

// Bad: Mocking internal methods
validator._checkRequiredProperties = vi.fn();

// Good: Test through public API
const result = validator.validate(...);

2. Brittle Mocks

// Bad: Exact call matching
expect(mockFn).toHaveBeenCalledWith(exact, args, here);

// Good: Flexible matchers
expect(mockFn).toHaveBeenCalledWith(
  expect.objectContaining({ type: 'nodes-base.slack' })
);

3. Mock Leakage

// Bad: Global mocks without cleanup
vi.mock('axios'); // At file level

// Good: Scoped mocks with cleanup
beforeEach(() => {
  vi.mock('axios');
});
afterEach(() => {
  vi.unmock('axios');
});

Integration Points

For services that work together, create integration tests:

describe('Validation Pipeline Integration', () => {
  it('validates complete workflow with all validators', async () => {
    // Use real services, only mock external dependencies
    const nodeRepo = createMockNodeRepository();
    const workflowValidator = new WorkflowValidator(
      nodeRepo,
      EnhancedConfigValidator // Real validator
    );
    
    const workflow = workflowFactory.withValidationErrors();
    const result = await workflowValidator.validateWorkflow(workflow);
    
    // Test that all validators work together correctly
    expect(result.errors).toContainEqual(
      expect.objectContaining({ 
        message: expect.stringContaining('Expression error') 
      })
    );
  });
});

This mocking strategy ensures tests are:

  • Fast (no real I/O)
  • Reliable (no external dependencies)
  • Maintainable (clear boundaries)
  • Realistic (use real implementations where possible)