Skip to content

Testing Guide - Companies House MCP Server

Overview

This guide outlines the testing strategy for the Companies House MCP Server. Tests are organized into three categories: unit, integration, and contract tests.

Test Structure

tests/
├── unit/           # Test individual functions and classes
├── integration/    # Test with real Companies House API
├── contract/       # Test MCP protocol compliance
└── fixtures/       # Shared test data
    └── companies.json

Running Tests

# Run all tests
npm test

# Run specific test suites
npm run test:unit        # Unit tests only
npm run test:integration # Integration tests only
npm run test:coverage    # Generate coverage report

# Run tests in watch mode
npm run test:watch

Test Categories

Unit Tests

Test individual components in isolation with mocked external dependencies.

What to test: - Pure functions (formatters, validators) - Business logic within tools - Error handling scenarios - Edge cases

Example:

// tests/unit/lib/cache.test.ts
describe('Cache', () => {
  it('should store and retrieve values', () => {
    const cache = new Cache(100);
    cache.set('key', 'value', 60);
    expect(cache.get('key')).toBe('value');
  });

  it('should respect TTL', async () => {
    const cache = new Cache(100);
    cache.set('key', 'value', 1); // 1 second TTL
    await new Promise(resolve => setTimeout(resolve, 1100));
    expect(cache.get('key')).toBeNull();
  });
});

Integration Tests

Test actual API interactions using known company numbers.

Known Test Companies: - 02050399 - ZENITH PRINT (UK) LIMITED (active, has PSCs and charges) - 00006400 - MARINE AND GENERAL MUTUAL LIFE ASSURANCE SOCIETY - 00445790 - TESCO PLC - 12345678 - Invalid (for error testing)

Note: The get_company_officers endpoint may return API service errors for some companies. Tests should handle this gracefully.

Example:

// tests/integration/search-companies.test.ts
describe('Search Companies Integration', () => {
  const client = new CompaniesHouseClient(process.env.COMPANIES_HOUSE_API_KEY!);

  it('should find Tesco when searching', async () => {
    const results = await client.searchCompanies('tesco', 5);

    expect(results).toContainEqual(
      expect.objectContaining({
        companyNumber: '00445790',
        title: expect.stringContaining('TESCO'),
        companyStatus: 'active'
      })
    );
  });

  it('should handle company not found', async () => {
    await expect(client.getCompanyProfile('99999999'))
      .rejects.toThrow('Resource not found');
  });
});

Contract Tests

Verify MCP protocol compliance and tool behavior.

What to test: - Tool registration and discovery - Input schema validation - Response format compliance - Error response format

Example:

// tests/contract/mcp-compliance.test.ts
describe('MCP Protocol Compliance', () => {
  let server: CompaniesHouseMCPServer;

  beforeEach(() => {
    server = new CompaniesHouseMCPServer('test', '1.0.0', 'test-key');
  });

  it('should expose tools with proper schema', () => {
    const tools = server.getTools();

    tools.forEach(tool => {
      expect(tool).toHaveProperty('name');
      expect(tool).toHaveProperty('description');
      expect(tool.inputSchema).toMatchObject({
        type: 'object',
        properties: expect.any(Object),
        required: expect.any(Array)
      });
    });
  });
});

Test Patterns

Mocking External Dependencies

Only mock external services, never internal modules:

// ✅ Good - Mock external API
jest.mock('node-fetch');

// ❌ Bad - Don't mock internal modules
jest.mock('../../../src/lib/client');

Using Fixtures

Store common test data in fixtures:

// tests/fixtures/companies.json
{
  "searchResults": [
    {
      "companyNumber": "00445790",
      "title": "TESCO PLC",
      "companyStatus": "active",
      "companyType": "plc",
      "dateOfCreation": "1947-11-27"
    }
  ],
  "companyProfile": {
    "company_name": "TESCO PLC",
    "company_number": "00445790",
    "company_status": "active",
    "type": "plc",
    "date_of_creation": "1947-11-27",
    "registered_office_address": {
      "address_line_1": "Tesco House",
      "postal_code": "EN14 5HW",
      "locality": "Welwyn Garden City",
      "country": "United Kingdom"
    }
  }
}

Testing Error Scenarios

Always test error paths:

describe('Error Handling', () => {
  it('should handle 401 unauthorized', async () => {
    const client = new CompaniesHouseClient('invalid-key');

    await expect(client.searchCompanies('test'))
      .rejects.toThrow('Invalid Companies House API key');
  });

  it('should format errors for MCP', async () => {
    const tool = new SearchCompaniesTool('invalid-key');
    const result = await tool.execute({ query: 'test' });

    expect(result).toEqual({
      isError: true,
      content: [{
        type: 'text',
        text: expect.stringContaining('Error:')
      }]
    });
  });
});

Best Practices

1. Test Behavior, Not Implementation

Focus on what the code does, not how it does it:

// ✅ Good - Tests behavior
it('should return formatted company data', async () => {
  const result = await tool.execute({ companyNumber: '00445790' });
  expect(result.content[0].text).toContain('TESCO PLC');
  expect(result.content[0].text).toContain('Status: active');
});

// ❌ Bad - Tests implementation details
it('should call formatAddress method', () => {
  // Don't test internal method calls
});

2. Use Descriptive Test Names

Test names should describe the scenario and expected outcome:

// ✅ Good
it('should return empty array when no companies match search query', async () => {

// ❌ Bad
it('search test', () => {

3. Keep Tests Independent

Each test should be able to run in isolation:

describe('CompaniesHouseClient', () => {
  let client: CompaniesHouseClient;

  beforeEach(() => {
    // Fresh instance for each test
    client = new CompaniesHouseClient('test-key');
  });

  afterEach(() => {
    // Cleanup if needed
    jest.clearAllMocks();
  });
});

4. Test Edge Cases

Don't just test the happy path:

describe('Pagination', () => {
  it('should handle empty results', async () => {
    const results = await client.searchCompanies('xyzxyzxyz123');
    expect(results).toEqual([]);
  });

  it('should respect limit parameter', async () => {
    const results = await client.searchCompanies('limited', 5);
    expect(results.length).toBeLessThanOrEqual(5);
  });

  it('should handle maximum limit', async () => {
    const results = await client.searchCompanies('test', 100);
    expect(results.length).toBeLessThanOrEqual(100);
  });
});

Environment Setup

For integration tests, set up your environment:

# .env.test
COMPANIES_HOUSE_API_KEY=your_test_api_key

# Or use environment variable
export COMPANIES_HOUSE_API_KEY=your_test_api_key

Continuous Integration

Tests run automatically on: - Every pull request to develop - Every push to develop or main

Failed tests will block merging.