Files
n8n-mcp-server/docs/development/testing.md
leonardsellem 2cd565cfa6 Initial commit of n8n MCP Server
A Model Context Protocol (MCP) server that integrates with n8n, providing tools for workflow and execution management via the n8n API.
2025-03-12 17:12:35 +01:00

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:

  1. Unit Tests: Test individual components in isolation
  2. Integration Tests: Test interactions between components
  3. 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 components
  • tests/integration/: Integration tests that test interactions between components
  • tests/e2e/: End-to-end tests that test the entire system
  • tests/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.ts has a corresponding test at tests/unit/api/n8n-client.test.ts
  • src/tools/workflow/list.ts has a corresponding test at tests/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

  1. Write tests first: Follow a test-driven development (TDD) approach when possible.
  2. Test behavior, not implementation: Focus on what a component does, not how it's implemented.
  3. Keep tests simple: Each test should test one behavior or aspect of functionality.
  4. Use descriptive test names: Test names should describe the expected behavior.
  5. Follow the AAA pattern: Arrange, Act, Assert (setup, execute, verify).

Mocking Best Practices

  1. Mock dependencies, not the unit under test: Only mock external dependencies, not the code you're testing.
  2. Use the minimum viable mock: Only mock the methods and behavior needed for the test.
  3. Ensure mock behavior is realistic: Mocks should behave similarly to the real implementation.
  4. Verify interactions with mocks: Use expect(mock).toHaveBeenCalled() to verify interactions.

Error Testing Best Practices

  1. Test error cases: Don't just test the happy path; test error handling too.
  2. Simulate errors with mocks: Use mocks to simulate error scenarios.
  3. Verify error messages: Ensure error messages are helpful and descriptive.

Performance Testing Considerations

  1. Monitor test performance: Slow tests can slow down development.
  2. Use test timeout values wisely: Set appropriate timeout values for async tests.
  3. Minimize redundant setup: Use beforeEach and beforeAll to 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.