test_example.py•15.8 kB
"""Example test file demonstrating MCP server testing best practices.
This file serves as a teaching example for how to test MCP servers
effectively. It demonstrates various testing patterns and techniques.
"""
import asyncio
import pytest
from unittest.mock import Mock, patch
from mcp.types import TextContent
from src.tools import hosts
class TestMCPToolExample:
"""Example test class demonstrating MCP tool testing patterns."""
@pytest.fixture
def sample_device_data(self):
"""Example fixture providing sample device data."""
return {
"id": 1,
"name": "example-server",
"display": "example-server",
"device_type": {"slug": "server", "display": "Server"},
"device_role": {"slug": "web-server", "display": "Web Server"},
"status": {"value": "active", "label": "Active"},
"primary_ip4": {
"address": "192.168.1.100/24",
"display": "192.168.1.100/24"
},
"site": {"slug": "datacenter-1", "display": "Datacenter 1"},
"rack": {"name": "R01", "display": "R01"},
"position": 10,
"created": "2025-01-01T00:00:00Z",
"last_updated": "2025-01-01T12:00:00Z"
}
@pytest.fixture
def mock_netbox_client(self, sample_device_data):
"""Example fixture providing a mocked NetBox client."""
client = Mock()
client.list_devices.return_value = [sample_device_data]
client.get_device.return_value = sample_device_data
client.search_devices.return_value = [sample_device_data]
return client
@pytest.fixture
def mock_state_client(self):
"""Example fixture providing a mocked state confidence client."""
client = Mock()
client.get_certainty_score.return_value = {
"certainty_score": 0.95,
"data_age_seconds": 300,
"last_verified": "2025-01-01T12:00:00Z"
}
return client
# Example 1: Basic Success Test
@pytest.mark.unit
@pytest.mark.asyncio
async def test_list_hosts_success_basic(self, mock_netbox_client, mock_state_client):
"""Example: Basic success test for list_hosts tool.
This test demonstrates:
- Simple success scenario testing
- Basic assertions
- Fixture usage
"""
# Arrange
arguments = {"limit": 10, "include_certainty": True}
# Act
result = await hosts.handle_list_hosts(
arguments, mock_netbox_client, mock_state_client
)
# Assert
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Found 1 host(s)" in result[0].text
assert "example-server" in result[0].text
# Example 2: Parameter Validation Test
@pytest.mark.unit
@pytest.mark.asyncio
async def test_list_hosts_with_filters(self, mock_netbox_client, mock_state_client):
"""Example: Testing with different parameter combinations.
This test demonstrates:
- Testing with various input parameters
- Verifying correct client method calls
- Parameter validation
"""
# Test with name filter
arguments = {"name": "example", "limit": 5}
await hosts.handle_list_hosts(arguments, mock_netbox_client, mock_state_client)
# Verify the client was called with correct parameters
mock_netbox_client.list_devices.assert_called_with(
name="example", primary_ip=None, role=None, limit=5
)
# Test with role filter
arguments = {"role": "web-server", "limit": 5}
await hosts.handle_list_hosts(arguments, mock_netbox_client, mock_state_client)
mock_netbox_client.list_devices.assert_called_with(
name=None, primary_ip=None, role="web-server", limit=5
)
# Example 3: Error Handling Test
@pytest.mark.unit
@pytest.mark.error_handling
@pytest.mark.asyncio
async def test_list_hosts_netbox_error(self, mock_state_client):
"""Example: Testing error handling scenarios.
This test demonstrates:
- Testing error conditions
- Verifying error messages
- Graceful error handling
"""
# Arrange - Create a client that raises an exception
mock_netbox_client = Mock()
mock_netbox_client.list_devices.side_effect = Exception("NetBox API error")
arguments = {"limit": 10}
# Act
result = await hosts.handle_list_hosts(
arguments, mock_netbox_client, mock_state_client
)
# Assert
assert len(result) == 1
assert "Error" in result[0].text
assert "NetBox" in result[0].text
# Example 4: Edge Case Test
@pytest.mark.unit
@pytest.mark.edge_cases
@pytest.mark.asyncio
async def test_list_hosts_empty_result(self, mock_state_client):
"""Example: Testing edge cases and boundary conditions.
This test demonstrates:
- Testing with empty results
- Edge case handling
- Boundary condition testing
"""
# Arrange - Create a client that returns empty results
mock_netbox_client = Mock()
mock_netbox_client.list_devices.return_value = []
arguments = {"limit": 10}
# Act
result = await hosts.handle_list_hosts(
arguments, mock_netbox_client, mock_state_client
)
# Assert
assert len(result) == 1
assert "Found 0 host(s)" in result[0].text
assert "[]" in result[0].text
# Example 5: State Confidence Integration Test
@pytest.mark.unit
@pytest.mark.asyncio
async def test_list_hosts_with_certainty_scores(self, mock_netbox_client, mock_state_client):
"""Example: Testing integration with state confidence system.
This test demonstrates:
- Testing optional integrations
- Verifying data enrichment
- Testing with external dependencies
"""
# Arrange
arguments = {"limit": 10, "include_certainty": True}
# Act
result = await hosts.handle_list_hosts(
arguments, mock_netbox_client, mock_state_client
)
# Assert
assert len(result) == 1
assert "Found 1 host(s)" in result[0].text
assert "certainty" in result[0].text.lower()
assert "0.95" in result[0].text # Certainty score should be included
# Verify state client was called
mock_state_client.get_certainty_score.assert_called()
# Example 6: Performance Test
@pytest.mark.performance
@pytest.mark.asyncio
async def test_list_hosts_performance(self, mock_netbox_client, mock_state_client):
"""Example: Basic performance testing.
This test demonstrates:
- Response time testing
- Performance assertions
- Basic benchmarking
"""
import time
# Arrange
arguments = {"limit": 10}
# Act
start_time = time.time()
result = await hosts.handle_list_hosts(
arguments, mock_netbox_client, mock_state_client
)
end_time = time.time()
# Assert
response_time = end_time - start_time
assert response_time < 0.1, f"Response too slow: {response_time:.3f}s"
assert len(result) == 1
# Example 7: Data Validation Test
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_host_data_structure(self, mock_netbox_client, mock_state_client, sample_device_data):
"""Example: Testing data structure and content.
This test demonstrates:
- Verifying data structure
- Content validation
- Field presence checking
"""
# Arrange
arguments = {"hostname": "example-server", "include_certainty": True}
# Act
result = await hosts.handle_get_host(
arguments, mock_netbox_client, mock_state_client
)
# Assert
assert len(result) == 1
result_text = result[0].text
# Verify all important fields are present
assert "example-server" in result_text
assert "Web Server" in result_text # Device role display
assert "Server" in result_text # Device type display
assert "Active" in result_text # Status
assert "192.168.1.100" in result_text # IP address
assert "Datacenter 1" in result_text # Site
assert "R01" in result_text # Rack
# Example 8: Concurrent Execution Test
@pytest.mark.integration
@pytest.mark.asyncio
async def test_concurrent_tool_calls(self, mock_netbox_client, mock_state_client):
"""Example: Testing concurrent execution.
This test demonstrates:
- Concurrent execution testing
- Async/await patterns
- Performance under load
"""
# Arrange
arguments = {"limit": 10}
# Act - Create multiple concurrent calls
tasks = [
hosts.handle_list_hosts(arguments, mock_netbox_client, mock_state_client)
for _ in range(5)
]
results = await asyncio.gather(*tasks)
# Assert
assert len(results) == 5
for result in results:
assert len(result) == 1
assert isinstance(result[0], TextContent)
# Example 9: Mock Verification Test
@pytest.mark.unit
@pytest.mark.asyncio
async def test_mock_verification(self, mock_netbox_client, mock_state_client):
"""Example: Verifying mock interactions.
This test demonstrates:
- Verifying mock calls
- Call argument validation
- Mock interaction testing
"""
# Arrange
arguments = {"name": "test", "limit": 5, "include_certainty": True}
# Act
await hosts.handle_list_hosts(arguments, mock_netbox_client, mock_state_client)
# Assert - Verify mock was called correctly
mock_netbox_client.list_devices.assert_called_once_with(
name="test", primary_ip=None, role=None, limit=5
)
# Verify state client was called for certainty scores
mock_state_client.get_certainty_score.assert_called()
# Example 10: Parameterized Test
@pytest.mark.unit
@pytest.mark.parametrize("limit,expected_text", [
(1, "Found 1 host(s)"),
(5, "Found 1 host(s)"),
(10, "Found 1 host(s)"),
(100, "Found 1 host(s)"),
])
@pytest.mark.asyncio
async def test_list_hosts_with_different_limits(self, mock_netbox_client, mock_state_client, limit, expected_text):
"""Example: Parameterized testing.
This test demonstrates:
- Parameterized testing
- Testing multiple scenarios
- Data-driven testing
"""
# Arrange
arguments = {"limit": limit}
# Act
result = await hosts.handle_list_hosts(
arguments, mock_netbox_client, mock_state_client
)
# Assert
assert len(result) == 1
assert expected_text in result[0].text
class TestAdvancedPatterns:
"""Advanced testing patterns and techniques."""
# Example 11: Context Manager Testing
@pytest.mark.unit
@pytest.mark.asyncio
async def test_with_context_manager(self):
"""Example: Testing with context managers and resource management."""
with patch('src.tools.hosts.VaultClient') as mock_vault_class:
# Setup mock
mock_vault_instance = Mock()
mock_vault_class.return_value = mock_vault_instance
mock_vault_instance.mint_netbox_token.return_value = "test-token"
# Test code that uses the context manager
# (This would be actual implementation code)
pass
# Example 12: Exception Testing
@pytest.mark.unit
@pytest.mark.error_handling
@pytest.mark.asyncio
async def test_specific_exception_handling(self):
"""Example: Testing specific exception types."""
mock_client = Mock()
mock_client.list_devices.side_effect = ConnectionError("Network unreachable")
# Test that specific exceptions are handled appropriately
result = await hosts.handle_list_hosts(
{"limit": 10}, mock_client, None
)
assert "Error" in result[0].text
assert "Connection" in result[0].text or "Network" in result[0].text
# Example 13: Custom Assertions
def assert_valid_mcp_response(self, result):
"""Custom assertion helper for MCP responses."""
assert isinstance(result, list), "Result should be a list"
assert len(result) > 0, "Result should not be empty"
assert all(isinstance(item, TextContent) for item in result), "All items should be TextContent"
assert all(len(item.text) > 0 for item in result), "All text content should be non-empty"
@pytest.mark.unit
@pytest.mark.asyncio
async def test_with_custom_assertions(self, mock_netbox_client, mock_state_client):
"""Example: Using custom assertion helpers."""
result = await hosts.handle_list_hosts(
{"limit": 10}, mock_netbox_client, mock_state_client
)
# Use custom assertion
self.assert_valid_mcp_response(result)
# Example 14: Test Configuration and Setup
class TestConfiguration:
"""Example: Test configuration and setup patterns."""
@pytest.fixture(scope="class")
def class_setup(self):
"""Class-level setup for expensive operations."""
# Setup that runs once per test class
return "class_setup_complete"
@pytest.fixture(autouse=True)
def auto_setup(self):
"""Auto-use fixture that runs for every test."""
# Setup that runs before every test
yield # Test runs here
# Cleanup that runs after every test
def test_with_class_setup(self, class_setup):
"""Test using class-level setup."""
assert class_setup == "class_setup_complete"
# Example 15: Test Documentation
class TestDocumentation:
"""Example: Well-documented test class.
This class demonstrates how to write well-documented tests
that serve as both tests and documentation.
"""
@pytest.mark.unit
@pytest.mark.asyncio
async def test_well_documented_example(self):
"""Test with comprehensive documentation.
This test demonstrates a well-documented test that explains:
- What is being tested
- Why it's important
- What the expected behavior is
- Any special considerations
The test verifies that the list_hosts tool correctly handles
the case where no hosts are found, which is important for
user experience and error handling.
Expected behavior:
- Should return a single TextContent response
- Should indicate that 0 hosts were found
- Should not raise an exception
- Should be user-friendly in the message
Special considerations:
- Empty results are common in new NetBox installations
- Users should get clear feedback about empty results
- The response should be consistent with other list operations
"""
# This would be the actual test implementation
# with detailed comments explaining each step
pass