A Model Context Protocol (MCP) server that integrates with n8n, providing tools for workflow and execution management via the n8n API.
14 KiB
Testing
This document describes the testing approach for the n8n MCP Server and provides guidelines for writing effective tests.
Overview
The n8n MCP Server uses Jest as its testing framework and follows a multi-level testing approach:
- Unit Tests: Test individual components in isolation
- Integration Tests: Test interactions between components
- End-to-End Tests: Test the entire system as a whole
Tests are organized in the tests/ directory, with a structure that mirrors the src/ directory.
Running Tests
Running All Tests
To run all tests:
npm test
This command runs all tests and outputs a summary of the results.
Running Tests with Coverage
To run tests with coverage reporting:
npm run test:coverage
This generates coverage reports in the coverage/ directory, including HTML reports that you can view in a browser.
Running Tests in Watch Mode
During development, you can run tests in watch mode, which will automatically rerun tests when files change:
npm run test:watch
Running Specific Tests
To run tests in a specific file or directory:
npx jest path/to/test-file.test.ts
Or to run tests matching a specific pattern:
npx jest -t "test pattern"
Test Structure
Tests are organized into the following directories:
tests/unit/: Unit tests for individual componentstests/integration/: Integration tests that test interactions between componentstests/e2e/: End-to-end tests that test the entire systemtests/mocks/: Shared test fixtures and mocks
Unit Tests
Unit tests are organized in a structure that mirrors the src/ directory. For example:
src/api/n8n-client.tshas a corresponding test attests/unit/api/n8n-client.test.tssrc/tools/workflow/list.tshas a corresponding test attests/unit/tools/workflow/list.test.ts
Integration Tests
Integration tests focus on testing interactions between components, such as:
- Testing that tools correctly use the API client
- Testing that resources correctly format data from the API
End-to-End Tests
End-to-end tests test the entire system, from the transport layer to the API client and back.
Writing Effective Tests
Unit Test Example
Here's an example of a unit test for a workflow tool:
// tests/unit/tools/workflow/list.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { getListWorkflowsToolDefinition, handleListWorkflows } from '../../../../src/tools/workflow/list.js';
import { N8nClient } from '../../../../src/api/n8n-client.js';
// Mock data
const mockWorkflows = [
{
id: '1234abc',
name: 'Test Workflow 1',
active: true,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-02T14:30:00.000Z'
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z'
}
];
describe('Workflow List Tool', () => {
describe('getListWorkflowsToolDefinition', () => {
it('should return the correct tool definition', () => {
const definition = getListWorkflowsToolDefinition();
expect(definition.name).toBe('workflow_list');
expect(definition.description).toBeTruthy();
expect(definition.inputSchema).toBeDefined();
expect(definition.inputSchema.properties).toHaveProperty('active');
expect(definition.inputSchema.required).toEqual([]);
});
});
describe('handleListWorkflows', () => {
it('should return all workflows when no filter is provided', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, {});
expect(mockClient.getWorkflows).toHaveBeenCalledWith(undefined);
expect(result.isError).toBeFalsy();
// Parse the JSON text to check the content
const content = JSON.parse(result.content[0].text);
expect(content).toHaveLength(2);
expect(content[0].id).toBe('1234abc');
expect(content[1].id).toBe('5678def');
});
it('should filter workflows by active status', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, { active: true });
expect(mockClient.getWorkflows).toHaveBeenCalledWith(true);
expect(result.isError).toBeFalsy();
// Parse the JSON text to check the content
const content = JSON.parse(result.content[0].text);
expect(content).toHaveLength(2);
});
it('should handle API errors', async () => {
// Mock the API client to throw an error
const mockClient = {
getWorkflows: jest.fn().mockRejectedValue(new Error('API error'))
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, {});
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('API error');
});
});
});
Integration Test Example
Here's an example of an integration test that tests the interaction between a resource handler and the API client:
// tests/integration/resources/static/workflows.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { handleWorkflowsRequest, WORKFLOWS_URI } from '../../../../src/resources/static/workflows.js';
import { N8nClient } from '../../../../src/api/n8n-client.js';
// Mock data
const mockWorkflows = [
{
id: '1234abc',
name: 'Test Workflow 1',
active: true,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-02T14:30:00.000Z'
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z'
}
];
describe('Workflows Resource Handler', () => {
it('should return a properly formatted response', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const response = await handleWorkflowsRequest(mockClient as unknown as N8nClient);
expect(mockClient.getWorkflows).toHaveBeenCalled();
expect(response.contents).toHaveLength(1);
expect(response.contents[0].uri).toBe(WORKFLOWS_URI);
expect(response.contents[0].mimeType).toBe('application/json');
// Parse the JSON text to check the content
const content = JSON.parse(response.contents[0].text);
expect(content).toHaveProperty('workflows');
expect(content.workflows).toHaveLength(2);
expect(content.count).toBe(2);
expect(content.workflows[0].id).toBe('1234abc');
});
it('should handle API errors', async () => {
// Mock the API client to throw an error
const mockClient = {
getWorkflows: jest.fn().mockRejectedValue(new Error('API error'))
};
await expect(handleWorkflowsRequest(mockClient as unknown as N8nClient))
.rejects
.toThrow('Failed to retrieve workflows');
});
});
End-to-End Test Example
Here's an example of an end-to-end test that tests the entire system:
// tests/e2e/workflow-operations.test.ts
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { MemoryServerTransport } from '@modelcontextprotocol/sdk/server/memory.js';
import { createServer } from '../../src/index.js';
describe('End-to-End Workflow Operations', () => {
let server: Server;
let transport: MemoryServerTransport;
beforeAll(async () => {
// Mock the environment
process.env.N8N_API_URL = 'http://localhost:5678/api/v1';
process.env.N8N_API_KEY = 'test-api-key';
// Create the server with a memory transport
transport = new MemoryServerTransport();
server = await createServer(transport);
});
afterAll(async () => {
await server.close();
});
it('should list workflows', async () => {
// Send a request to list workflows
const response = await transport.sendRequest({
jsonrpc: '2.0',
id: '1',
method: 'callTool',
params: {
name: 'workflow_list',
arguments: {}
}
});
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
expect(response.result.content[0].type).toBe('text');
// Parse the JSON text to check the content
const content = JSON.parse(response.result.content[0].text);
expect(Array.isArray(content)).toBe(true);
});
it('should retrieve a workflow by ID', async () => {
// Send a request to get a workflow
const response = await transport.sendRequest({
jsonrpc: '2.0',
id: '2',
method: 'callTool',
params: {
name: 'workflow_get',
arguments: {
id: '1234abc'
}
}
});
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
expect(response.result.content[0].type).toBe('text');
// Parse the JSON text to check the content
const content = JSON.parse(response.result.content[0].text);
expect(content).toHaveProperty('id');
expect(content.id).toBe('1234abc');
});
});
Test Fixtures and Mocks
To avoid duplication and improve test maintainability, common test fixtures and mocks are stored in the tests/mocks/ directory.
Axios Mock
The Axios HTTP client is mocked using axios-mock-adapter to simulate HTTP responses without making actual API calls:
// tests/mocks/axios-mock.ts
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
// Create a new instance of the mock adapter
export const axiosMock = new MockAdapter(axios);
// Helper function to reset the mock adapter before each test
export function resetAxiosMock() {
axiosMock.reset();
}
n8n API Fixtures
Common fixtures for n8n API responses are stored in a shared file:
// tests/mocks/n8n-fixtures.ts
export const mockWorkflows = [
{
id: '1234abc',
name: 'Test Workflow 1',
active: true,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-02T14:30:00.000Z',
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.start',
position: [100, 200],
parameters: {}
}
],
connections: {}
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z',
nodes: [],
connections: {}
}
];
export const mockExecutions = [
{
id: 'exec123',
workflowId: '1234abc',
workflowName: 'Test Workflow 1',
status: 'success',
startedAt: '2025-03-10T15:00:00.000Z',
finishedAt: '2025-03-10T15:01:00.000Z',
mode: 'manual'
},
{
id: 'exec456',
workflowId: '1234abc',
workflowName: 'Test Workflow 1',
status: 'error',
startedAt: '2025-03-09T12:00:00.000Z',
finishedAt: '2025-03-09T12:00:10.000Z',
mode: 'manual'
}
];
Test Environment
The test environment is configured in jest.config.js and babel.config.js. Key configurations include:
- TypeScript support via Babel
- ES module support
- Coverage reporting
The tests/test-setup.ts file contains global setup code that runs before tests:
// tests/test-setup.ts
import { jest } from '@jest/globals';
import { resetAxiosMock } from './mocks/axios-mock';
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
resetAxiosMock();
});
Best Practices
General Testing Guidelines
- Write tests first: Follow a test-driven development (TDD) approach when possible.
- Test behavior, not implementation: Focus on what a component does, not how it's implemented.
- Keep tests simple: Each test should test one behavior or aspect of functionality.
- Use descriptive test names: Test names should describe the expected behavior.
- Follow the AAA pattern: Arrange, Act, Assert (setup, execute, verify).
Mocking Best Practices
- Mock dependencies, not the unit under test: Only mock external dependencies, not the code you're testing.
- Use the minimum viable mock: Only mock the methods and behavior needed for the test.
- Ensure mock behavior is realistic: Mocks should behave similarly to the real implementation.
- Verify interactions with mocks: Use
expect(mock).toHaveBeenCalled()to verify interactions.
Error Testing Best Practices
- Test error cases: Don't just test the happy path; test error handling too.
- Simulate errors with mocks: Use mocks to simulate error scenarios.
- Verify error messages: Ensure error messages are helpful and descriptive.
Performance Testing Considerations
- Monitor test performance: Slow tests can slow down development.
- Use test timeout values wisely: Set appropriate timeout values for async tests.
- Minimize redundant setup: Use
beforeEachandbeforeAllto avoid redundant setup.
Continuous Integration
Tests are run automatically in CI environments on pull requests and commits to the main branch. The CI configuration ensures tests pass before code can be merged.
CI Test Requirements
- All tests must pass
- Test coverage must not decrease
- Linting checks must pass
Debugging Tests
Console Output
You can use console.log() statements in your tests to debug issues:
it('should do something', () => {
const result = doSomething();
console.log('Result:', result);
expect(result).toBe(expectedValue);
});
When running tests with Jest, console output will be displayed for failing tests by default.
Using the Debugger
You can also use the Node.js debugger with Jest:
node --inspect-brk node_modules/.bin/jest --runInBand path/to/test
Then connect to the debugger with Chrome DevTools or VS Code.
Conclusion
Thorough testing is essential for maintaining a reliable and robust n8n MCP Server. By following these guidelines and examples, you can write effective tests that help ensure your code works as expected and catches issues early.