# Testing

Comprehensive guide to testing your MCP server and tools.

## Testing Strategy

A complete testing strategy includes:

1. **Unit Tests** - Test individual tools in isolation
2. **Integration Tests** - Test tools with real AIDP API
3. **End-to-End Tests** - Test complete MCP server workflows
4. **Performance Tests** - Test under load
5. **Security Tests** - Test authentication and authorization

***

## Unit Testing

### Setup

```bash
npm install --save-dev jest @types/jest
```

### Test Structure

```typescript
// tools/__tests__/search-businesses.test.ts
import { searchBusinesses } from '../search-businesses';
import { AIDPClient } from '@aidp/sdk';

// Mock AIDP client
jest.mock('@aidp/sdk');

describe('search_businesses tool', () => {
  let mockClient: jest.Mocked<AIDPClient>;

  beforeEach(() => {
    mockClient = new AIDPClient({ apiKey: 'test' }) as jest.Mocked<AIDPClient>;
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should search for businesses successfully', async () => {
    // Arrange
    const mockResponse = {
      success: true,
      data: {
        businesses: [{ id: 'biz_1', name: 'Test Business' }],
        total: 1,
      },
    };
    mockClient.search.mockResolvedValue(mockResponse);

    // Act
    const result = await searchBusinesses.handler({
      query: 'coffee shops',
      location: { lat: 45.5231, lon: -122.6765 },
    });

    // Assert
    expect(result.success).toBe(true);
    expect(result.data.businesses).toHaveLength(1);
    expect(mockClient.search).toHaveBeenCalledWith({
      query: 'coffee shops',
      location: { lat: 45.5231, lon: -122.6765, distance: '10km' },
      limit: 10,
    });
  });

  it('should handle missing parameters', async () => {
    // Act
    const result = await searchBusinesses.handler({});

    // Assert
    expect(result.success).toBe(false);
    expect(result.error.code).toBe('INVALID_PARAMETERS');
  });

  it('should handle API errors', async () => {
    // Arrange
    mockClient.search.mockRejectedValue(new Error('API Error'));

    // Act
    const result = await searchBusinesses.handler({
      query: 'coffee',
      location: { lat: 45.5231, lon: -122.6765 },
    });

    // Assert
    expect(result.success).toBe(false);
    expect(result.error.code).toBe('SEARCH_FAILED');
  });
});
```

### Running Unit Tests

```bash
npm test
```

***

## Integration Testing

Test tools with real AIDP API using sandbox environment.

### Setup

```typescript
// tools/__tests__/integration/search.integration.test.ts
import { AIDPClient } from '@aidp/sdk';
import { searchBusinesses } from '../../search-businesses';

describe('search_businesses integration', () => {
  let client: AIDPClient;

  beforeAll(() => {
    client = new AIDPClient({
      apiKey: process.env.AIDP_TEST_API_KEY,
      environment: 'sandbox',
    });
  });

  it('should search for real businesses', async () => {
    const result = await searchBusinesses.handler({
      query: 'coffee shops',
      location: { lat: 45.5231, lon: -122.6765, distance: '5km' },
    });

    expect(result.success).toBe(true);
    expect(result.data.businesses).toBeDefined();
    expect(Array.isArray(result.data.businesses)).toBe(true);
  }, 10000); // 10 second timeout

  it('should handle rate limiting', async () => {
    // Make many requests quickly
    const promises = Array(150)
      .fill(null)
      .map(() =>
        searchBusinesses.handler({
          query: 'test',
          location: { lat: 45.5231, lon: -122.6765 },
        })
      );

    const results = await Promise.all(promises);
    const rateLimited = results.some((r) => !r.success && r.error.code === 'RATE_LIMIT_EXCEEDED');

    expect(rateLimited).toBe(true);
  }, 30000);
});
```

### Running Integration Tests

```bash
AIDP_TEST_API_KEY=your-test-key npm run test:integration
```

***

## End-to-End Testing

Test complete MCP server workflows.

### Setup

```typescript
// __tests__/e2e/mcp-server.e2e.test.ts
import { MCPServer } from '@aidp/mcp-server';
import axios from 'axios';

describe('MCP Server E2E', () => {
  let server: MCPServer;
  const baseURL = 'http://localhost:3001';

  beforeAll(async () => {
    server = new MCPServer({
      port: 3001,
      aidpApiKey: process.env.AIDP_TEST_API_KEY,
    });
    await server.start();
  });

  afterAll(async () => {
    await server.stop();
  });

  it('should start and respond to health checks', async () => {
    const response = await axios.get(`${baseURL}/health`);

    expect(response.status).toBe(200);
    expect(response.data.status).toBe('healthy');
  });

  it('should list available tools', async () => {
    const response = await axios.get(`${baseURL}/mcp/tools`);

    expect(response.status).toBe(200);
    expect(response.data.tools).toBeDefined();
    expect(response.data.tools.length).toBeGreaterThan(0);
  });

  it('should execute search tool', async () => {
    const response = await axios.post(`${baseURL}/mcp/call`, {
      tool: 'search_businesses',
      parameters: {
        query: 'coffee shops',
        location: { lat: 45.5231, lon: -122.6765 },
      },
    });

    expect(response.status).toBe(200);
    expect(response.data.success).toBe(true);
    expect(response.data.data.businesses).toBeDefined();
  });

  it('should handle invalid tool names', async () => {
    try {
      await axios.post(`${baseURL}/mcp/call`, {
        tool: 'invalid_tool',
        parameters: {},
      });
      fail('Should have thrown error');
    } catch (error) {
      expect(error.response.status).toBe(404);
      expect(error.response.data.error.code).toBe('TOOL_NOT_FOUND');
    }
  });
});
```

***

## Performance Testing

Test server performance under load.

### Using Artillery

Install Artillery:

```bash
npm install -g artillery
```

Create `artillery-config.yml`:

```yaml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
      name: 'Warm up'
    - duration: 120
      arrivalRate: 50
      name: 'Sustained load'
    - duration: 60
      arrivalRate: 100
      name: 'Peak load'

scenarios:
  - name: 'Search businesses'
    flow:
      - post:
          url: '/mcp/call'
          json:
            tool: 'search_businesses'
            parameters:
              query: 'coffee shops'
              location:
                lat: 45.5231
                lon: -122.6765

  - name: 'Get business'
    flow:
      - post:
          url: '/mcp/call'
          json:
            tool: 'get_business'
            parameters:
              businessId: 'biz_test123'
```

Run performance test:

```bash
artillery run artillery-config.yml
```

### Performance Metrics

Monitor these metrics:

* **Response Time**: p50, p95, p99
* **Throughput**: Requests per second
* **Error Rate**: Failed requests percentage
* **Resource Usage**: CPU, memory, network

***

## Security Testing

### Authentication Tests

```typescript
describe('MCP Server Security', () => {
  it('should reject requests without API key', async () => {
    try {
      await axios.post('http://localhost:3000/mcp/call', {
        tool: 'search_businesses',
        parameters: { query: 'test' },
      });
      fail('Should have rejected request');
    } catch (error) {
      expect(error.response.status).toBe(401);
    }
  });

  it('should reject invalid API keys', async () => {
    try {
      await axios.post(
        'http://localhost:3000/mcp/call',
        {
          tool: 'search_businesses',
          parameters: { query: 'test' },
        },
        {
          headers: { Authorization: 'Bearer invalid-key' },
        }
      );
      fail('Should have rejected request');
    } catch (error) {
      expect(error.response.status).toBe(401);
    }
  });

  it('should enforce rate limits', async () => {
    const requests = Array(150)
      .fill(null)
      .map(() =>
        axios
          .post(
            'http://localhost:3000/mcp/call',
            {
              tool: 'search_businesses',
              parameters: { query: 'test' },
            },
            {
              headers: { Authorization: `Bearer ${process.env.AIDP_API_KEY}` },
            }
          )
          .catch((e) => e.response)
      );

    const responses = await Promise.all(requests);
    const rateLimited = responses.some((r) => r.status === 429);

    expect(rateLimited).toBe(true);
  });
});
```

***

## Test Coverage

### Measuring Coverage

```bash
npm test -- --coverage
```

### Coverage Goals

Aim for:

* **Statements**: > 80%
* **Branches**: > 75%
* **Functions**: > 80%
* **Lines**: > 80%

### Coverage Report

```
--------------------|---------|----------|---------|---------|
File                | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files           |   85.23 |    78.45 |   82.11 |   85.67 |
 tools/             |   88.12 |    81.23 |   85.45 |   88.34 |
  search.ts         |   92.34 |    85.67 |   90.12 |   92.45 |
  get-business.ts   |   87.23 |    79.45 |   83.12 |   87.56 |
--------------------|---------|----------|---------|---------|
```

***

## Continuous Integration

### GitHub Actions

Create `.github/workflows/test.yml`:

```yaml
name: Test MCP Server

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm test

      - name: Run integration tests
        env:
          AIDP_TEST_API_KEY: ${{ secrets.AIDP_TEST_API_KEY }}
        run: npm run test:integration

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
```

***

## Manual Testing

### Using cURL

```bash
# Test health endpoint
curl http://localhost:3000/health

# List tools
curl http://localhost:3000/mcp/tools

# Call search tool
curl -X POST http://localhost:3000/mcp/call \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "search_businesses",
    "parameters": {
      "query": "coffee shops",
      "location": {"lat": 45.5231, "lon": -122.6765}
    }
  }'
```

### Using Postman

1. Import MCP server collection
2. Set environment variables
3. Run test suite
4. Review results

***

## Debugging Tests

### Enable Debug Logging

```bash
DEBUG=aidp:* npm test
```

### Use Test Debugger

```json
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Jest Debug",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--runInBand", "--no-cache"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    }
  ]
}
```

***

## Best Practices

### 1. Test Isolation

Each test should be independent:

```typescript
beforeEach(() => {
  // Reset state
  jest.clearAllMocks();
  cache.clear();
});
```

### 2. Use Test Fixtures

Create reusable test data:

```typescript
// __fixtures__/businesses.ts
export const mockBusiness = {
  id: 'biz_test123',
  name: 'Test Business',
  category: 'restaurants',
  // ...
};

export const mockSearchResponse = {
  success: true,
  data: {
    businesses: [mockBusiness],
    total: 1,
  },
};
```

### 3. Test Edge Cases

```typescript
it('should handle empty results', async () => {
  mockClient.search.mockResolvedValue({
    success: true,
    data: { businesses: [], total: 0 },
  });

  const result = await searchBusinesses.handler({
    query: 'nonexistent',
    location: { lat: 0, lon: 0 },
  });

  expect(result.data.businesses).toHaveLength(0);
});
```

### 4. Test Timeouts

```typescript
it('should timeout long-running requests', async () => {
  mockClient.search.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 35000)));

  await expect(searchBusinesses.handler({ query: 'test' })).rejects.toThrow('Timeout');
}, 40000);
```

***

## Troubleshooting

### Tests Failing Intermittently

**Issue**: Flaky tests

**Solution**: Add retries for integration tests:

```typescript
jest.retryTimes(3);
```

### Slow Tests

**Issue**: Tests take too long

**Solution**: Run tests in parallel:

```bash
npm test -- --maxWorkers=4
```

### Memory Leaks

**Issue**: Tests consume too much memory

**Solution**: Clear mocks and reset state:

```typescript
afterEach(() => {
  jest.clearAllMocks();
  jest.restoreAllMocks();
});
```

***

## Next Steps

* [MCP Server Setup →](https://amistan.gitbook.io/aidp-docs/for-developers/mcp-integration/setup)
* [Standard Tools →](https://amistan.gitbook.io/aidp-docs/for-developers/mcp-integration/tools)
* [Custom Tools →](https://amistan.gitbook.io/aidp-docs/for-developers/mcp-integration/custom-tools)

***

## Support

* 📧 Email: <testing@aidp.dev>
* 💬 GitHub Discussions: [github.com/aidp/platform/discussions](https://github.com/aidp/platform/discussions)
* 📚 Documentation: [docs.aidp.dev/mcp/testing](https://docs.aidp.dev/mcp/testing)
