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>
This commit is contained in:
227
tests/unit/examples/using-n8n-nodes-base-mock.test.ts
Normal file
227
tests/unit/examples/using-n8n-nodes-base-mock.test.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user