Testing
GatheRing uses pytest for testing with a focus on test-driven development (TDD).
Test Structure
tests/
├── unit/ # Unit tests
│ ├── test_agents.py
│ ├── test_circles.py
│ ├── test_memory.py
│ └── test_tools.py
├── integration/ # Integration tests
│ ├── test_api.py
│ ├── test_database.py
│ └── test_websocket.py
├── e2e/ # End-to-end tests
│ └── test_workflows.py
├── test_auth_persistence.py # Auth lifecycle (v1.0)
├── test_sql_security.py # SQL injection prevention (v1.0)
├── test_path_traversal.py # Path traversal defense (v1.0)
├── test_pipeline_validation.py # DAG validation, cycle rejection (v1.0)
├── test_pipeline_execution.py # Node execution, retry, circuit breaker (v1.0)
├── test_pipeline_cancellation.py # Cancellation, timeout (v1.0)
├── test_scheduler_recovery.py # Crash recovery, deduplication (v1.0)
├── test_tool_validation.py # JSON Schema validation (v1.0)
├── test_event_bus_concurrency.py # Parallel handling, dedup, ordering (v1.0)
├── test_async_database.py # Async DB concurrency (v1.0)
├── test_rate_limit_tiers.py # Per-endpoint rate limiting (v1.0)
├── test_advisory_locks.py # Multi-instance coordination (v1.0)
├── test_graceful_shutdown.py # Shutdown draining (v1.0)
├── conftest.py # Shared fixtures
└── fixtures/ # Test data
├── agents.json
└── conversations.json
v1.0 Test Categories
Category |
Tests |
What They Prove |
|---|---|---|
Auth lifecycle |
~50 |
Token creation, expiry, blacklist persistence, constant-time auth |
Pipeline execution |
~41 |
DAG traversal, node dispatch, retry, circuit breaker, cancellation, timeout |
Scheduler recovery |
~28 |
Action dispatch, crash recovery, dedup, race conditions |
Tool validation |
~15 |
JSON Schema validation, async execution, workspace paths |
Event concurrency |
~7 |
Parallel handling, dedup, ordering, backpressure |
Rate limiting |
~5 |
Per-endpoint tiers, 429 response, Retry-After headers |
Advisory locks |
~5 |
Multi-instance coordination, fail-closed behavior |
Graceful shutdown |
~5 |
Ordered teardown, readiness probe, request draining |
Running Tests
All Tests
pytest
With Coverage
pytest --cov=gathering --cov-report=html
Specific Tests
# Single file
pytest tests/unit/test_agents.py
# Single test
pytest tests/unit/test_agents.py::test_create_agent
# By marker
pytest -m "not slow"
# By keyword
pytest -k "memory"
Verbose Output
pytest -v --tb=short
Writing Tests
Basic Test
# tests/unit/test_agents.py
import pytest
from gathering.agents import Agent
def test_create_agent():
"""Test basic agent creation."""
# Arrange
config = {"name": "TestAgent", "provider": "openai", "model": "gpt-4o"}
# Act
agent = Agent.from_config(config)
# Assert
assert agent.name == "TestAgent"
assert agent.provider == "openai"
assert agent.model == "gpt-4o"
Async Tests
import pytest
@pytest.mark.asyncio
async def test_agent_process_message():
"""Test async message processing."""
agent = Agent(name="Test", provider="anthropic", model="claude-sonnet-4-20250514")
response = await agent.process_message("Hello")
assert response is not None
assert len(response) > 0
Parametrized Tests
import pytest
@pytest.mark.parametrize("provider,model,expected_class", [
("openai", "gpt-4o", "OpenAIProvider"),
("anthropic", "claude-sonnet-4-20250514", "AnthropicProvider"),
("ollama", "llama3.2", "OllamaProvider"),
])
def test_provider_initialization(provider, model, expected_class):
"""Test provider initialization."""
agent = Agent(name="Test", provider=provider, model=model)
assert agent.llm_provider.__class__.__name__ == expected_class
Test Classes
class TestAgentMemory:
"""Tests for agent memory functionality."""
def test_memory_stores_messages(self, agent):
"""Test that messages are stored."""
agent.process_message("Hello")
history = agent.memory.get_history()
assert len(history) >= 1
def test_memory_clears(self, agent):
"""Test memory clearing."""
agent.process_message("Hello")
agent.memory.clear()
assert len(agent.memory.get_history()) == 0
Fixtures
Basic Fixtures
# tests/conftest.py
import pytest
from gathering.agents import Agent
from gathering.db import Database
@pytest.fixture
def agent():
"""Create a test agent."""
return Agent(name="TestAgent", provider="ollama", model="llama3.2")
@pytest.fixture
async def db():
"""Create a test database connection."""
database = Database(test=True)
await database.connect()
yield database
await database.disconnect()
Scoped Fixtures
@pytest.fixture(scope="session")
def app():
"""Create app once per test session."""
from gathering.api.app import create_app
return create_app(testing=True)
@pytest.fixture(scope="function")
async def clean_db(db):
"""Clean database before each test."""
await db.execute("TRUNCATE agent.agents CASCADE")
yield db
Factory Fixtures
@pytest.fixture
def agent_factory():
"""Factory for creating test agents."""
created = []
def _create(name="Test", provider="ollama", model="llama3.2", **kwargs):
agent = Agent(name=name, provider=provider, model=model, **kwargs)
created.append(agent)
return agent
yield _create
# Cleanup
for agent in created:
agent.cleanup()
def test_with_factory(agent_factory):
agent1 = agent_factory(name="Agent1")
agent2 = agent_factory(name="Agent2", provider="openai", model="gpt-4o")
# ...
Mocking
Basic Mocking
from unittest.mock import Mock, patch
def test_with_mock():
mock_llm = Mock()
mock_llm.generate.return_value = "Mocked response"
agent = Agent(name="Test", provider="openai", model="gpt-4o", llm=mock_llm)
response = agent.process_message("Hello")
assert response == "Mocked response"
mock_llm.generate.assert_called_once()
Patching
from unittest.mock import patch
@patch("gathering.agents.llm_client")
def test_with_patch(mock_client):
mock_client.generate.return_value = Mock(
content=[Mock(text="Response")]
)
agent = Agent(name="Test", provider="anthropic", model="claude-sonnet-4-20250514")
response = agent.process_message("Hello")
assert response == "Response"
Async Mocking
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_async_mock():
mock_db = AsyncMock()
mock_db.fetch_one.return_value = {"id": 1, "name": "Test"}
result = await mock_db.fetch_one("SELECT * FROM agents WHERE id = 1")
assert result["name"] == "Test"
API Testing
Testing Endpoints
import pytest
from httpx import AsyncClient
from gathering.api.app import app
@pytest.fixture
async def client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.mark.asyncio
async def test_list_agents(client):
response = await client.get("/agents")
assert response.status_code == 200
assert isinstance(response.json(), list)
@pytest.mark.asyncio
async def test_create_agent(client):
response = await client.post(
"/agents",
json={
"name": "NewAgent",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514"
},
)
assert response.status_code == 201
assert response.json()["name"] == "NewAgent"
Testing WebSocket
from starlette.testclient import TestClient
def test_websocket_connection():
client = TestClient(app)
with client.websocket_connect("/ws/circles/test") as ws:
ws.send_json({"type": "ping"})
response = ws.receive_json()
assert response["type"] == "pong"
Database Testing
Test Database
# tests/conftest.py
import pytest
from gathering.db import Database
@pytest.fixture(scope="session")
async def test_db():
"""Create test database."""
db = Database(url="postgresql://localhost/gathering_test")
await db.connect()
await db.run_migrations()
yield db
await db.disconnect()
@pytest.fixture(autouse=True)
async def clean_tables(test_db):
"""Clean tables before each test."""
await test_db.execute("TRUNCATE agent.agents CASCADE")
yield
Testing Queries
@pytest.mark.asyncio
async def test_insert_agent(test_db):
result = await test_db.execute(
"""
INSERT INTO agent.agents (name, provider, model)
VALUES ($1, $2, $3)
RETURNING id
""",
"TestAgent",
"openai",
"gpt-4o",
)
assert result["id"] is not None
agent = await test_db.fetch_one(
"SELECT * FROM agent.agents WHERE id = $1",
result["id"],
)
assert agent["name"] == "TestAgent"
assert agent["provider"] == "openai"
Test Markers
# pytest.ini
[pytest]
markers =
slow: marks tests as slow
integration: marks integration tests
e2e: marks end-to-end tests
Usage:
@pytest.mark.slow
def test_large_dataset():
# ...
@pytest.mark.integration
async def test_database_integration():
# ...
Running:
# Skip slow tests
pytest -m "not slow"
# Only integration tests
pytest -m integration
Test Coverage
Configuration
# .coveragerc
[run]
source = gathering
omit =
*/tests/*
*/__init__.py
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
Running Coverage
# Generate coverage report
pytest --cov=gathering --cov-report=html
# Check minimum coverage
pytest --cov=gathering --cov-fail-under=80
Best Practices
1. Follow AAA Pattern
def test_something():
# Arrange - Set up test data
data = create_test_data()
# Act - Perform the action
result = do_something(data)
# Assert - Verify the result
assert result == expected
2. One Assertion Per Test (When Possible)
# Good - focused tests
def test_agent_has_name():
agent = Agent(name="Test")
assert agent.name == "Test"
def test_agent_has_default_provider():
agent = Agent(name="Test")
assert agent.provider == "anthropic" # default provider
3. Use Descriptive Names
# Good
def test_agent_raises_error_when_name_is_empty():
with pytest.raises(ValueError):
Agent(name="")
# Bad
def test_agent_error():
# ...
4. Isolate Tests
Tests should not depend on each other or external state.
5. Mock External Services
@patch("gathering.llm.anthropic_client")
def test_without_real_api(mock_client):
# Test without calling real API
pass
Continuous Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: gathering_test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install -r requirements-dev.txt
- name: Run tests
run: |
pytest --cov=gathering --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3