Files
n8n-mcp/tests/unit/examples/using-n8n-nodes-base-mock.test.ts
czlonkowski b5210e5963 feat: add comprehensive performance benchmark tracking system
- Create benchmark test suites for critical operations:
  - Node loading performance
  - Database query performance
  - Search operations performance
  - Validation performance
  - MCP tool execution performance

- Add GitHub Actions workflow for benchmark tracking:
  - Runs on push to main and PRs
  - Uses github-action-benchmark for historical tracking
  - Comments on PRs with performance results
  - Alerts on >10% performance regressions
  - Stores results in GitHub Pages

- Create benchmark infrastructure:
  - Custom Vitest benchmark configuration
  - JSON reporter for CI results
  - Result formatter for github-action-benchmark
  - Performance threshold documentation

- Add supporting utilities:
  - SQLiteStorageService for benchmark database setup
  - MCPEngine wrapper for testing MCP tools
  - Test factories for generating benchmark data
  - Enhanced NodeRepository with benchmark methods

- Document benchmark system:
  - Comprehensive benchmark guide in docs/BENCHMARKS.md
  - Performance thresholds in .github/BENCHMARK_THRESHOLDS.md
  - README for benchmarks directory
  - Integration with existing test suite

The benchmark system will help monitor performance over time and catch regressions before they reach production.

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

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

227 lines
7.2 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { getNodeTypes, mockNodeBehavior, resetAllMocks } from '../__mocks__/n8n-nodes-base';
// Example service that uses n8n-nodes-base
class WorkflowService {
async getNodeDescription(nodeName: string) {
const nodeTypes = getNodeTypes();
const node = nodeTypes.getByName(nodeName);
return node?.description;
}
async executeNode(nodeName: string, context: any) {
const nodeTypes = getNodeTypes();
const node = nodeTypes.getByName(nodeName);
if (!node?.execute) {
throw new Error(`Node ${nodeName} does not have an execute method`);
}
return node.execute.call(context);
}
async validateSlackMessage(channel: string, text: string) {
if (!channel || !text) {
throw new Error('Channel and text are required');
}
const nodeTypes = getNodeTypes();
const slackNode = nodeTypes.getByName('slack');
if (!slackNode) {
throw new Error('Slack node not found');
}
// Check if required properties exist
const channelProp = slackNode.description.properties.find(p => p.name === 'channel');
const textProp = slackNode.description.properties.find(p => p.name === 'text');
return !!(channelProp && textProp);
}
}
// Mock the module at the top level
vi.mock('n8n-nodes-base', () => ({
getNodeTypes: vi.fn(() => {
const { getNodeTypes } = require('../__mocks__/n8n-nodes-base');
return getNodeTypes();
})
}));
describe('WorkflowService with n8n-nodes-base mock', () => {
let service: WorkflowService;
beforeEach(() => {
resetAllMocks();
service = new WorkflowService();
});
describe('getNodeDescription', () => {
it('should get webhook node description', async () => {
const description = await service.getNodeDescription('webhook');
expect(description).toBeDefined();
expect(description?.name).toBe('webhook');
expect(description?.group).toContain('trigger');
expect(description?.webhooks).toBeDefined();
});
it('should get httpRequest node description', async () => {
const description = await service.getNodeDescription('httpRequest');
expect(description).toBeDefined();
expect(description?.name).toBe('httpRequest');
expect(description?.version).toBe(3);
const methodProp = description?.properties.find(p => p.name === 'method');
expect(methodProp).toBeDefined();
expect(methodProp?.options).toHaveLength(6);
});
});
describe('executeNode', () => {
it('should execute httpRequest node with custom response', async () => {
// Override the httpRequest node behavior for this test
mockNodeBehavior('httpRequest', {
execute: vi.fn(async function(this: any) {
const url = this.getNodeParameter('url', 0);
return [[{
json: {
statusCode: 200,
url,
customData: 'mocked response'
}
}]];
})
});
const mockContext = {
getInputData: vi.fn(() => [{ json: { input: 'data' } }]),
getNodeParameter: vi.fn((name: string) => {
if (name === 'url') return 'https://test.com/api';
return '';
})
};
const result = await service.executeNode('httpRequest', mockContext);
expect(result).toBeDefined();
expect(result[0][0].json).toMatchObject({
statusCode: 200,
url: 'https://test.com/api',
customData: 'mocked response'
});
});
it('should execute slack node and track calls', async () => {
const mockContext = {
getInputData: vi.fn(() => [{ json: { message: 'test' } }]),
getNodeParameter: vi.fn((name: string, index: number) => {
const params: Record<string, string> = {
resource: 'message',
operation: 'post',
channel: '#general',
text: 'Hello from test!'
};
return params[name] || '';
}),
getCredentials: vi.fn(async () => ({ token: 'mock-token' }))
};
const result = await service.executeNode('slack', mockContext);
expect(result).toBeDefined();
expect(result[0][0].json).toMatchObject({
ok: true,
channel: '#general',
message: {
text: 'Hello from test!'
}
});
// Verify the mock was called
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('channel', 0, '');
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('text', 0, '');
});
it('should throw error for non-executable node', async () => {
// Create a trigger-only node
mockNodeBehavior('webhook', {
execute: undefined // Remove execute method
});
await expect(
service.executeNode('webhook', {})
).rejects.toThrow('Node webhook does not have an execute method');
});
});
describe('validateSlackMessage', () => {
it('should validate slack message parameters', async () => {
const isValid = await service.validateSlackMessage('#general', 'Hello');
expect(isValid).toBe(true);
});
it('should throw error for missing parameters', async () => {
await expect(
service.validateSlackMessage('', 'Hello')
).rejects.toThrow('Channel and text are required');
await expect(
service.validateSlackMessage('#general', '')
).rejects.toThrow('Channel and text are required');
});
it('should handle missing slack node', async () => {
// Override getNodeTypes to return undefined for slack
const getNodeTypes = vi.fn(() => ({
getByName: vi.fn((name: string) => {
if (name === 'slack') return undefined;
return null;
}),
getByNameAndVersion: vi.fn()
}));
vi.mocked(require('n8n-nodes-base').getNodeTypes).mockImplementation(getNodeTypes);
await expect(
service.validateSlackMessage('#general', 'Hello')
).rejects.toThrow('Slack node not found');
});
});
describe('complex workflow scenarios', () => {
it('should handle if node branching', async () => {
const mockContext = {
getInputData: vi.fn(() => [
{ json: { status: 'active' } },
{ json: { status: 'inactive' } },
{ json: { status: 'active' } },
]),
getNodeParameter: vi.fn()
};
const result = await service.executeNode('if', mockContext);
expect(result).toHaveLength(2); // true and false branches
expect(result[0]).toHaveLength(2); // items at index 0 and 2
expect(result[1]).toHaveLength(1); // item at index 1
});
it('should handle merge node combining inputs', async () => {
const mockContext = {
getInputData: vi.fn((inputIndex?: number) => {
if (inputIndex === 0) return [{ json: { source: 'input1' } }];
if (inputIndex === 1) return [{ json: { source: 'input2' } }];
return [{ json: { source: 'input1' } }];
}),
getNodeParameter: vi.fn(() => 'append')
};
const result = await service.executeNode('merge', mockContext);
expect(result).toBeDefined();
expect(result[0]).toHaveLength(1);
});
});
});