WASM MCP Server
by beekmarks
# WASM MCP Server Testing Strategy
This document outlines the testing approach for the WASM MCP Server implementation, covering unit tests, integration tests, and end-to-end testing.
## 1. Unit Testing
### Server Component (`server.ts`)
#### Tool Registration Tests
```typescript
describe('MCP Server Tool Registration', () => {
let server: McpServer;
beforeEach(() => {
server = createServer();
});
test('should register calculator tool', () => {
const tools = server._registeredTools;
expect(tools).toHaveProperty('calculate');
expect(tools.calculate).toHaveProperty('callback');
});
test('should register storage tool', () => {
const tools = server._registeredTools;
expect(tools).toHaveProperty('set-storage');
expect(tools['set-storage']).toHaveProperty('callback');
});
});
```
#### Calculator Tool Tests
```typescript
describe('Calculator Tool', () => {
let server: McpServer;
let calculatorTool: any;
beforeEach(() => {
server = createServer();
calculatorTool = server._registeredTools.calculate;
});
test('should add numbers correctly', async () => {
const result = await calculatorTool.callback({
operation: 'add',
a: 5,
b: 3
});
expect(result.content[0].text).toBe('8');
});
test('should handle division by zero', async () => {
await expect(calculatorTool.callback({
operation: 'divide',
a: 10,
b: 0
})).rejects.toThrow('Division by zero');
});
test('should validate input types', async () => {
await expect(calculatorTool.callback({
operation: 'add',
a: 'not a number',
b: 3
})).rejects.toThrow();
});
});
```
#### Storage Tests
```typescript
describe('Storage Operations', () => {
let server: McpServer;
let storageTool: any;
let storageResource: any;
beforeEach(() => {
server = createServer();
storageTool = server._registeredTools['set-storage'];
storageResource = server._registeredResources['storage://{key}'];
});
test('should store and retrieve values', async () => {
// Store value
await storageTool.callback({
key: 'test-key',
value: 'test-value'
});
// Retrieve value
const uri = new URL('storage://test-key');
const result = await storageResource.readCallback(uri, { key: 'test-key' });
expect(result.contents[0].text).toBe('test-value');
});
test('should handle missing keys', async () => {
const uri = new URL('storage://nonexistent');
const result = await storageResource.readCallback(uri, { key: 'nonexistent' });
expect(result.contents[0].text).toBe('Key not found');
});
});
```
### Transport Component (`browser-transport.ts`)
```typescript
describe('Browser Transport', () => {
let transport: BrowserTransport;
beforeEach(() => {
transport = new BrowserTransport();
});
test('should initialize correctly', async () => {
await transport.start();
expect(transport.isConnected()).toBe(true);
});
test('should handle message sending', async () => {
const message = { type: 'test', data: 'value' };
const response = await transport.send(message);
expect(response).toBeDefined();
});
test('should handle connection errors', async () => {
// Simulate connection failure
jest.spyOn(transport, 'start').mockRejectedValue(new Error('Connection failed'));
await expect(transport.start()).rejects.toThrow('Connection failed');
});
});
```
## 2. Integration Testing
### Server-Transport Integration
```typescript
describe('Server-Transport Integration', () => {
let server: McpServer;
let transport: BrowserTransport;
beforeEach(async () => {
transport = new BrowserTransport();
await transport.start();
server = createServer();
await server.connect(transport);
});
test('should handle tool execution through transport', async () => {
const message = {
type: 'tool',
name: 'calculate',
params: {
operation: 'add',
a: 5,
b: 3
}
};
const response = await transport.send(message);
expect(response.content[0].text).toBe('8');
});
test('should handle resource access through transport', async () => {
// First store a value
await transport.send({
type: 'tool',
name: 'set-storage',
params: {
key: 'test-key',
value: 'test-value'
}
});
// Then retrieve it
const response = await transport.send({
type: 'resource',
uri: 'storage://test-key'
});
expect(response.contents[0].text).toBe('test-value');
});
});
```
### UI Integration Tests
```typescript
describe('UI Integration', () => {
beforeEach(async () => {
// First set up test environment
await setupTestEnvironment();
// Then initialize UI
setupCalculatorUI();
});
test('should handle calculator UI interaction', async () => {
const num1Input = document.getElementById('num1') as HTMLInputElement;
const num2Input = document.getElementById('num2') as HTMLInputElement;
const operationSelect = document.getElementById('operation') as HTMLSelectElement;
const calcButton = document.getElementById('calcButton') as HTMLButtonElement;
const output = document.getElementById('calcOutput');
// Test each operation
const operations = [
{ op: 'add', a: 5, b: 3, expected: '8' },
{ op: 'subtract', a: 10, b: 4, expected: '6' },
{ op: 'multiply', a: 6, b: 7, expected: '42' },
{ op: 'divide', a: 15, b: 3, expected: '5' }
];
for (const { op, a, b, expected } of operations) {
operationSelect.value = op;
num1Input.value = a.toString();
num2Input.value = b.toString();
fireEvent.click(calcButton);
// Wait for the calculation to complete
await new Promise(resolve => setTimeout(resolve, 100));
expect(output?.textContent).toBe(`Result: ${expected}`);
}
});
test('should handle calculator error cases', async () => {
const num1Input = document.getElementById('num1') as HTMLInputElement;
const num2Input = document.getElementById('num2') as HTMLInputElement;
const operationSelect = document.getElementById('operation') as HTMLSelectElement;
const calcButton = document.getElementById('calcButton') as HTMLButtonElement;
const output = document.getElementById('calcOutput');
// Test division by zero
operationSelect.value = 'divide';
num1Input.value = '10';
num2Input.value = '0';
fireEvent.click(calcButton);
// Wait for the error to be displayed
await new Promise(resolve => setTimeout(resolve, 100));
expect(output?.textContent).toContain('Error');
expect(output?.textContent).toContain('Division by zero');
});
});
describe('Storage UI Integration', () => {
beforeEach(async () => {
// First set up test environment
await setupTestEnvironment();
// Then initialize UI
setupStorageUI();
});
test('should handle storage UI interaction', async () => {
const keyInput = document.getElementById('storageKey') as HTMLInputElement;
const valueInput = document.getElementById('storageValue') as HTMLInputElement;
const setButton = document.getElementById('setStorageButton') as HTMLButtonElement;
const getButton = document.getElementById('getStorageButton') as HTMLButtonElement;
const output = document.getElementById('storageOutput');
// Set value
keyInput.value = 'test-key';
valueInput.value = 'test-value';
fireEvent.click(setButton);
// Wait for the storage operation to complete
await new Promise(resolve => setTimeout(resolve, 100));
expect(output?.textContent).toContain('Value stored successfully');
// Get value
valueInput.value = '';
fireEvent.click(getButton);
// Wait for the retrieval to complete
await new Promise(resolve => setTimeout(resolve, 100));
expect(output?.textContent).toContain('test-value');
});
test('should handle missing storage keys', async () => {
const keyInput = document.getElementById('storageKey') as HTMLInputElement;
const getButton = document.getElementById('getStorageButton') as HTMLButtonElement;
const output = document.getElementById('storageOutput');
keyInput.value = 'nonexistent-key';
fireEvent.click(getButton);
// Wait for the retrieval to complete
await new Promise(resolve => setTimeout(resolve, 100));
expect(output?.textContent).toContain('Key not found');
});
});
```
## 3. End-to-End Testing
### Test Scenarios
```typescript
describe('End-to-End Workflows', () => {
beforeEach(async () => {
// Set up complete environment
await setupTestEnvironment();
});
test('complete calculator workflow', async () => {
// Test all operations
const operations = ['add', 'subtract', 'multiply', 'divide'];
const testCases = [
{ a: 5, b: 3, expected: ['8', '2', '15', '1.6666666666666667'] }
];
for (const { a, b, expected } of testCases) {
for (let i = 0; i < operations.length; i++) {
const operation = operations[i];
const result = await executeCalculation(operation, a, b);
expect(result).toBe(expected[i]);
}
}
});
test('complete storage workflow', async () => {
// Store multiple values
const testData = [
{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' }
];
for (const { key, value } of testData) {
await setStorageValue(key, value);
const retrieved = await getStorageValue(key);
expect(retrieved).toBe(value);
}
});
});
```
## 4. Performance Testing
```typescript
describe('Performance Tests', () => {
test('calculator performance', async () => {
const startTime = performance.now();
// Perform 1000 calculations
for (let i = 0; i < 1000; i++) {
await executeCalculation('add', i, i + 1);
}
const endTime = performance.now();
const duration = endTime - startTime;
expect(duration).toBeLessThan(1000); // Should complete in less than 1 second
});
test('storage performance', async () => {
const startTime = performance.now();
// Perform 1000 storage operations
for (let i = 0; i < 1000; i++) {
await setStorageValue(`key${i}`, `value${i}`);
await getStorageValue(`key${i}`);
}
const endTime = performance.now();
const duration = endTime - startTime;
expect(duration).toBeLessThan(2000); // Should complete in less than 2 seconds
});
});
```
## 5. Error Testing
```typescript
describe('Error Handling', () => {
test('server initialization errors', async () => {
// Test invalid server configuration
expect(() => {
new McpServer({ name: '', version: '' });
}).toThrow();
});
test('transport errors', async () => {
// Test connection failures
const transport = new BrowserTransport();
jest.spyOn(transport, 'start').mockRejectedValue(new Error('Network error'));
await expect(transport.start()).rejects.toThrow('Network error');
});
test('calculator errors', async () => {
// Test various error conditions
const errorCases = [
{ operation: 'divide', a: 1, b: 0, error: 'Division by zero' },
{ operation: 'invalid', a: 1, b: 1, error: 'Invalid operation' },
{ operation: 'add', a: 'invalid', b: 1, error: 'Invalid input' }
];
for (const { operation, a, b, error } of errorCases) {
await expect(executeCalculation(operation, a, b)).rejects.toThrow(error);
}
});
});
```
## Test Configuration
### Jest Configuration
```javascript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['./jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
```
### Test Setup
```typescript
// jest.setup.ts
import '@testing-library/jest-dom';
global.beforeEach(() => {
// Reset DOM
document.body.innerHTML = '';
// Reset storage
localStorage.clear();
// Reset server state
jest.resetModules();
});
```
## Running Tests
```bash
# Run all tests
npm test
# Run tests with coverage
npm test -- --coverage
# Run specific test file
npm test -- server.test.ts
# Run tests in watch mode
npm test -- --watch
```
## Continuous Integration
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v2
```
## Test Coverage Requirements
- Minimum 80% line coverage
- Minimum 80% branch coverage
- Minimum 80% function coverage
- Critical paths must have 100% coverage:
- Server initialization
- Tool registration
- Resource handling
- Error handling
## Manual Testing Checklist
1. Server Initialization
- [ ] Server starts successfully
- [ ] Tools are registered
- [ ] Resources are available
- [ ] UI is enabled
2. Calculator Operations
- [ ] All operations work correctly
- [ ] Error handling works
- [ ] UI updates properly
- [ ] Performance is acceptable
3. Storage Operations
- [ ] Can store values
- [ ] Can retrieve values
- [ ] Handles missing keys
- [ ] Updates UI correctly
4. Error Scenarios
- [ ] Server initialization failures
- [ ] Network errors
- [ ] Invalid inputs
- [ ] Resource not found
5. Browser Compatibility
- [ ] Chrome
- [ ] Firefox
- [ ] Safari
- [ ] Edge