Testing Guidelines - iowarp/iowarp-mcps GitHub Wiki
Unit Tests
Create comprehensive unit tests for each tool to ensure reliability and correctness.
Test Structure
import pytest
import asyncio
from unittest.mock import Mock, patch
from your_mcp.server import app
class TestYourMCP:
@pytest.fixture
async def server(self):
"""Setup test server instance"""
return app
@pytest.mark.asyncio
async def test_tool_normal_operation(self, server):
"""Test tool with valid inputs"""
result = await server.call_tool("tool_name", {"param": "valid_value"})
assert result[0].text == "expected_output"
@pytest.mark.asyncio
async def test_tool_invalid_input(self, server):
"""Test tool with invalid inputs"""
result = await server.call_tool("tool_name", {"param": ""})
assert "Error" in result[0].text
@pytest.mark.asyncio
async def test_tool_missing_parameter(self, server):
"""Test tool with missing required parameters"""
result = await server.call_tool("tool_name", {})
assert "required" in result[0].text.lower()
Testing Best Practices
Normal Operation Testing
- Test with typical, valid inputs
- Verify expected outputs and formats
- Check return types and structures
- Validate tool schema compliance
Error Handling Testing
- Test with invalid input types
- Test with missing required parameters
- Test with malformed data
- Test with boundary conditions
- Test with extremely large inputs
Edge Cases
@pytest.mark.asyncio
async def test_empty_file(self, server):
"""Test handling of empty files"""
with patch('builtins.open', mock_open(read_data="")):
result = await server.call_tool("read_file", {"path": "empty.txt"})
assert "empty" in result[0].text.lower()
@pytest.mark.asyncio
async def test_large_dataset(self, server):
"""Test with large dataset"""
large_data = ["data"] * 10000
result = await server.call_tool("process_data", {"data": large_data})
assert result is not None
Async Testing Support
import pytest_asyncio
@pytest_asyncio.fixture
async def async_setup():
"""Setup for async tests"""
# Async setup code
yield
# Async cleanup code
@pytest.mark.asyncio
async def test_async_operation(async_setup):
"""Test async operations"""
result = await some_async_function()
assert result == expected_value
Integration Tests
Test with actual MCP clients to ensure end-to-end functionality.
Universal Client Testing
# Install client dependencies
uv pip install -r bin/requirements.txt
# Test with different LLM providers
python bin/wrp.py --conf=bin/confs/Gemini.yaml --test-mode
# Test specific tool
python bin/wrp.py --tool="your_tool" --params='{"param": "value"}'
Unified Launcher Testing
# Test server launch
uvx iowarp-mcps your-server
# Test with specific configuration
uvx iowarp-mcps your-server --config=test-config.json
Process Testing
import subprocess
import json
import asyncio
async def test_server_process():
"""Test server as separate process"""
# Start server process
process = await asyncio.create_subprocess_exec(
"python", "-m", "your_mcp.server",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
# Wait for server to start
await asyncio.sleep(2)
# Test communication
# ... MCP protocol testing
# Cleanup
process.terminate()
await process.wait()
Protocol Verification
import json
from mcp.client import ClientSession
async def test_mcp_protocol():
"""Test MCP protocol compliance"""
# Connect to server
session = ClientSession()
# Test tool listing
tools = await session.list_tools()
assert len(tools) > 0
# Test tool execution
result = await session.call_tool("tool_name", {"param": "value"})
assert result.content[0].text is not None
# Test error handling
try:
await session.call_tool("nonexistent_tool", {})
assert False, "Should have raised exception"
except Exception as e:
assert "not found" in str(e).lower()
Test Coverage
Coverage Requirements
- Aim for >90% code coverage
- Cover all public functions and methods
- Test all error paths
- Include integration test coverage
Coverage Tools
# Install coverage tools
pip install pytest-cov coverage
# Run tests with coverage
pytest --cov=your_mcp tests/
# Generate coverage report
coverage html
coverage report --show-missing
Coverage Configuration
# .coveragerc
[run]
source = your_mcp
omit =
*/tests/*
*/venv/*
*/build/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
Performance Testing
Load Testing
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
async def performance_test():
"""Test tool performance under load"""
start_time = time.time()
# Run multiple concurrent requests
tasks = []
for i in range(100):
task = asyncio.create_task(
server.call_tool("tool_name", {"param": f"value_{i}"})
)
tasks.append(task)
results = await asyncio.gather(*tasks)
end_time = time.time()
# Verify all requests succeeded
assert all(result is not None for result in results)
# Check performance metrics
total_time = end_time - start_time
requests_per_second = len(tasks) / total_time
print(f"Processed {len(tasks)} requests in {total_time:.2f}s")
print(f"Rate: {requests_per_second:.2f} requests/second")
# Assert performance requirements
assert requests_per_second > 10 # Minimum performance threshold
Memory Testing
import psutil
import gc
def test_memory_usage():
"""Test memory usage during operations"""
process = psutil.Process()
initial_memory = process.memory_info().rss
# Perform memory-intensive operation
large_data = generate_large_dataset()
result = process_large_data(large_data)
# Force garbage collection
del large_data
gc.collect()
final_memory = process.memory_info().rss
memory_increase = final_memory - initial_memory
# Assert memory usage is reasonable
assert memory_increase < 100 * 1024 * 1024 # Less than 100MB increase
Continuous Integration
GitHub Actions Configuration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-asyncio
pip install -e .
- name: Run tests
run: |
pytest --cov=your_mcp --cov-report=xml tests/
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Test Organization
Directory Structure
tests/
├── unit/
│ ├── test_tools.py
│ ├── test_resources.py
│ └── test_utils.py
├── integration/
│ ├── test_client_interaction.py
│ └── test_protocol_compliance.py
├── performance/
│ ├── test_load.py
│ └── test_memory.py
└── fixtures/
├── sample_data.json
└── test_files/
Test Configuration
# conftest.py
import pytest
import asyncio
from pathlib import Path
@pytest.fixture(scope="session")
def event_loop():
"""Create event loop for async tests"""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture
def sample_data():
"""Load sample test data"""
data_path = Path(__file__).parent / "fixtures" / "sample_data.json"
with open(data_path) as f:
return json.load(f)
@pytest.fixture
def temp_file():
"""Create temporary file for testing"""
import tempfile
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write("test content")
temp_path = f.name
yield temp_path
# Cleanup
import os
os.unlink(temp_path)