Testing Guide¶
This guide covers testing practices and strategies for the A2A Registry project.
Test Structure¶
The project uses pytest for testing with the following structure:
tests/
├── __init__.py
├── test_basic.py # Basic functionality tests
├── test_server.py # FastAPI server tests
├── conftest.py # Shared fixtures (if needed)
├── unit/ # Unit tests
├── integration/ # Integration tests
└── fixtures/ # Test data files
Running Tests¶
Basic Test Commands¶
# Run all tests
make test
# Run with coverage
make test-cov
# Run specific test file
pytest tests/test_server.py
# Run specific test
pytest tests/test_server.py::test_register_agent
# Run with verbose output
pytest -v
# Run tests in parallel
pytest -n auto
Test Categories¶
# Unit tests only
pytest tests/unit/
# Integration tests only
pytest tests/integration/
# Fast tests (skip slow ones)
pytest -m "not slow"
# Run only failed tests from last run
pytest --lf
Writing Tests¶
Test Structure¶
Follow the Arrange-Act-Assert pattern:
def test_register_agent_success():
# Arrange
agent_card = create_test_agent_card()
client = TestClient(app)
# Act
response = client.post("/agents", json={"agent_card": agent_card})
# Assert
assert response.status_code == 200
assert response.json()["success"] is True
Fixtures¶
Use pytest fixtures for common test setup:
import pytest
from fastapi.testclient import TestClient
from a2a_registry.server import create_app
@pytest.fixture
def app():
"""Create test application instance."""
return create_app()
@pytest.fixture
def client(app):
"""Create test client."""
return TestClient(app)
@pytest.fixture
def sample_agent_card():
"""Sample agent card for testing."""
return {
"name": "test-agent",
"description": "A test agent",
"url": "http://localhost:3000",
"version": "0.420.0",
"protocol_version": "0.3.0",
"skills": [
{
"id": "test_skill",
"description": "A test skill"
}
]
}
def test_register_agent(client, sample_agent_card):
response = client.post("/agents", json={"agent_card": sample_agent_card})
assert response.status_code == 200
Parameterized Tests¶
Test multiple scenarios with parameterized tests:
import pytest
@pytest.mark.parametrize("agent_name,expected_status", [
("valid-agent", 200),
("", 400), # Empty name should fail
("a" * 256, 400), # Too long name should fail
])
def test_register_agent_names(client, agent_name, expected_status):
agent_card = {
"name": agent_name,
"description": "Test agent",
"url": "http://localhost:3000",
"version": "0.420.0",
"protocol_version": "0.3.0",
"skills": []
}
response = client.post("/agents", json={"agent_card": agent_card})
assert response.status_code == expected_status
Unit Tests¶
Unit tests focus on individual components in isolation.
Testing Storage Layer¶
import pytest
from a2a_registry.storage import RegistryStorage
@pytest.fixture
def storage():
return RegistryStorage()
@pytest.mark.asyncio
async def test_register_agent(storage, sample_agent_card):
# Test successful registration
result = await storage.register_agent(sample_agent_card)
assert result is True
# Test agent can be retrieved
retrieved = await storage.get_agent(sample_agent_card["name"])
assert retrieved == sample_agent_card
@pytest.mark.asyncio
async def test_search_agents(storage, sample_agent_card):
await storage.register_agent(sample_agent_card)
# Search by name
results = await storage.search_agents("test")
assert len(results) == 1
assert results[0]["name"] == sample_agent_card["name"]
# Search by skill
results = await storage.search_agents("test_skill")
assert len(results) == 1
Testing Business Logic¶
def test_agent_card_validation():
"""Test agent card validation logic."""
from a2a_registry.server import validate_agent_card
# Valid card
valid_card = {...}
assert validate_agent_card(valid_card) is True
# Missing required field
invalid_card = valid_card.copy()
del invalid_card["name"]
assert validate_agent_card(invalid_card) is False
Integration Tests¶
Integration tests verify that components work together correctly.
API Integration Tests¶
@pytest.mark.asyncio
async def test_full_agent_lifecycle(client):
"""Test complete agent registration/discovery/deletion cycle."""
agent_card = create_test_agent_card()
# 1. Register agent
response = client.post("/agents", json={"agent_card": agent_card})
assert response.status_code == 200
agent_id = response.json()["agent_id"]
# 2. Verify agent appears in listings
response = client.get("/agents")
assert response.status_code == 200
agents = response.json()["agents"]
assert any(agent["name"] == agent_id for agent in agents)
# 3. Search for agent
response = client.post("/agents/search", json={"query": "test"})
assert response.status_code == 200
assert len(response.json()["agents"]) > 0
# 4. Get specific agent
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 200
assert response.json()["agent_card"]["name"] == agent_id
# 5. Delete agent
response = client.delete(f"/agents/{agent_id}")
assert response.status_code == 200
# 6. Verify agent is gone
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 404
gRPC Integration Tests¶
import grpc
import pytest
from a2a_registry.proto.generated import registry_pb2, registry_pb2_grpc
@pytest.fixture
def grpc_channel():
"""Create gRPC channel for testing."""
# This would need a running gRPC server
channel = grpc.insecure_channel('localhost:50051')
yield channel
channel.close()
@pytest.fixture
def grpc_stub(grpc_channel):
"""Create gRPC stub."""
return registry_pb2_grpc.A2ARegistryServiceStub(grpc_channel)
@pytest.mark.integration
def test_grpc_agent_registration(grpc_stub):
"""Test agent registration via gRPC."""
# Create agent card
agent_card = registry_pb2.AgentCard(
name="grpc-test-agent",
description="gRPC test agent",
url="http://localhost:3000",
version="0.420.0",
protocol_version="0.3.0"
)
registry_card = registry_pb2.RegistryAgentCard(agent_card=agent_card)
request = registry_pb2.StoreAgentCardRequest(
registry_agent_card=registry_card,
upsert=True
)
# Store agent
response = grpc_stub.StoreAgentCard(request)
assert response.success is True
# Retrieve agent
get_request = registry_pb2.GetAgentCardRequest(agent_id="grpc-test-agent")
get_response = grpc_stub.GetAgentCard(get_request)
assert get_response.found is True
assert get_response.registry_agent_card.agent_card.name == "grpc-test-agent"
Error Testing¶
Test error conditions and edge cases:
def test_register_agent_invalid_data(client):
"""Test error handling for invalid agent data."""
# Missing required fields
invalid_data = {"agent_card": {"name": "test"}} # Missing required fields
response = client.post("/agents", json=invalid_data)
assert response.status_code == 400
# Invalid JSON
response = client.post("/agents", data="invalid json",
headers={"Content-Type": "application/json"})
assert response.status_code == 422
def test_get_nonexistent_agent(client):
"""Test retrieving non-existent agent."""
response = client.get("/agents/nonexistent")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_delete_nonexistent_agent(client):
"""Test deleting non-existent agent."""
response = client.delete("/agents/nonexistent")
assert response.status_code == 404
Performance Tests¶
Test performance characteristics:
import time
import pytest
@pytest.mark.slow
def test_concurrent_registrations(client):
"""Test handling of concurrent agent registrations."""
import concurrent.futures
def register_agent(agent_id):
agent_card = create_test_agent_card(name=f"agent-{agent_id}")
response = client.post("/agents", json={"agent_card": agent_card})
return response.status_code == 200
# Register 100 agents concurrently
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(register_agent, i) for i in range(100)]
results = [future.result() for future in futures]
# All registrations should succeed
assert all(results)
# Verify all agents are registered
response = client.get("/agents")
assert response.json()["count"] == 100
@pytest.mark.slow
def test_large_search_performance(client):
"""Test search performance with many agents."""
# Register 1000 agents
for i in range(1000):
agent_card = create_test_agent_card(name=f"perf-agent-{i}")
client.post("/agents", json={"agent_card": agent_card})
# Time search operation
start_time = time.time()
response = client.post("/agents/search", json={"query": "perf"})
end_time = time.time()
assert response.status_code == 200
assert response.json()["count"] == 1000
assert end_time - start_time < 1.0 # Should complete within 1 second
Test Data Management¶
Using Test Fixtures¶
Create reusable test data:
# tests/fixtures/agent_cards.py
def minimal_agent_card():
return {
"name": "minimal-agent",
"description": "Minimal test agent",
"url": "http://localhost:3000",
"version": "0.420.0",
"protocol_version": "0.3.0",
"skills": []
}
def weather_agent_card():
return {
"name": "weather-agent",
"description": "Weather information agent",
"url": "http://weather.example.com",
"version": "0.420.0",
"protocol_version": "0.3.0",
"skills": [
{"id": "get_weather", "description": "Get current weather"},
{"id": "get_forecast", "description": "Get weather forecast"}
]
}
Database Test Helpers¶
For future database implementations:
@pytest.fixture
def clean_database():
"""Ensure clean database state for each test."""
# Setup: Clear database
clear_test_database()
yield
# Teardown: Clear database
clear_test_database()
def clear_test_database():
"""Clear all test data from database."""
# Implementation depends on storage backend
pass
Continuous Integration¶
Tests run automatically in CI. See .github/workflows/ci.yml
:
- name: Run tests
run: |
pytest --cov=src/a2a_registry --cov-report=xml --cov-report=term
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Test Configuration¶
Configure pytest in pyproject.toml
:
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --tb=short --strict-markers"
asyncio_mode = "auto"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
Coverage Goals¶
Maintain high test coverage:
- Minimum: 80% overall coverage
- Target: 90%+ coverage for core modules
- Critical paths: 100% coverage for registration/discovery logic
Best Practices¶
- Test Naming: Use descriptive test names that explain what is being tested
- Independence: Tests should not depend on each other
- Fast Feedback: Unit tests should run quickly (< 100ms each)
- Clear Assertions: Use specific assertions with meaningful error messages
- Test Data: Use factories or fixtures for test data creation
- Error Cases: Test both success and failure scenarios
- Documentation: Document complex test scenarios
Debugging Tests¶
Debug Failed Tests¶
# Run with debugging info
pytest -vvv --tb=long
# Drop into debugger on failure
pytest --pdb
# Run only failed tests
pytest --lf -vvv
Test Isolation¶
# Run single test in isolation
pytest tests/test_server.py::test_specific_function -s -vvv
# Run with fresh imports
pytest --forked
This comprehensive testing guide ensures reliable, maintainable code with good test coverage.