- 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>
8.1 KiB
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)