Testing Strategy
Detailed testing patterns, organization, and requirements for the project.
Coverage Requirements
100% coverage required on production code.
Included:
- All code in src/ except explicitly excluded files (see below)
Excluded:
- server.py (FastAPI entrypoint - validate server behavior with integration tests)
- **/agent.py (pure ADK configuration - tests are the upstream project's responsibility)
- **/prompt.py (prompt templates - validate agent behavior with evaluations)
- **/__init__.py (module initialization)
Test Organization
Directory Structure
Tests mirror source structure:
tests/
conftest.py # Shared fixtures, mocks, and test environment setup
test_callbacks.py # Tests for src/agent_foundation/callbacks.py
test_tools.py # Tests for src/agent_foundation/tools.py
...
Naming Conventions
- Files:
test_<module>.pymirroring source module name - Functions:
test_<what>_<condition>_<expected> - Classes: Group related tests for the same module/class (style preference)
Shared Fixtures
All reusable fixtures go in tests/conftest.py:
- Type hint fixture definitions with both parameters and return types
- Use pytest-mock type aliases for returns: MockType, AsyncMockType
- Factory pattern (not context managers)
Fixture Patterns
Type Hints
Fixture definitions (strict in conftest.py):
from pytest_mock import MockerFixture
from pytest_mock.plugin import MockType
@pytest.fixture
def mock_session(mocker: MockerFixture) -> MockType:
"""Create a mock ADK session."""
return mocker.MagicMock(spec=...)
Test functions (relaxed):
- Don't type hint custom fixtures (pytest handles DI by name)
- Optional type hints on built-in fixtures for IDE support
Environment Mocking
No base env vars in pytest_configure(): No module in the test import graph reads env vars at module level. server.py does (initialize_environment at line 26), but it's never imported during collection (PEP 562 lazy loading, coverage-excluded). Environment variables are set in test fixtures using mocker.patch.dict:
def test_config_with_custom_region(mocker: MockerFixture) -> None:
"""Test configuration with non-default region."""
mocker.patch.dict(os.environ, {"GOOGLE_CLOUD_LOCATION": "us-west1"})
# Test config loading with custom region
If a future import chain triggers env var reads at collection time, add direct os.environ assignments in pytest_configure() (see section below).
ADK Mock Strategy
Using Conftest Fixtures
Prefer fixtures from conftest.py for standard mocks:
- mock_state - ADK state object
- mock_session - ADK session
- mock_context - ReadonlyContext with user_id
Never Import Mock Classes
Never import mock classes directly in test files. Always use or add a fixture in conftest.py. For edge cases requiring custom internal structure, add a specific named fixture (e.g., mock_event_non_text_content) rather than importing the class.
Mirror Real Interfaces
ADK mocks must exactly mirror real ADK interfaces:
- Use spec= parameter to enforce interface
- Include all properties and methods used in code
- Match return types and signatures
pytest_configure()
Special case: unittest.mock before pytest-mock available
pytest_configure() runs before test collection, so pytest-mock isn't available yet.
Use unittest.mock here for setup that must happen before tests load:
def pytest_configure() -> None:
"""Configure test environment before test collection."""
from unittest.mock import Mock, patch
# Patch load_dotenv to prevent loading real .env
patch("dotenv.load_dotenv").start()
# Patch google.auth.default to prevent ADC lookup
mock_creds = Mock(token="fake", valid=True, expired=False)
patch("google.auth.default", return_value=(mock_creds, "test-project")).start()
patch("google.auth._default.default", return_value=(mock_creds, "test-project")).start()
# Environment variables: No module in the test import graph reads env vars at
# module level. server.py does (initialize_environment(ServerEnv)), but it's
# never imported during collection (PEP 562 lazy loading, coverage-excluded).
# If a future import chain triggers env var reads at collection time, set
# defaults here using direct assignment:
# import os
# os.environ["KEY"] = "value"
See tests/conftest.py for the complete implementation with detailed lifecycle comments.
Pydantic Validation Testing
Validation Timing
Pydantic validates at model creation, not at property access:
# Validation happens here ✓
config = ServerEnv.model_validate(env_dict)
# NOT here ✗
value = config.some_property
Testing Validation
Expect ValidationError at model creation:
import pytest
from pydantic import ValidationError
def test_config_invalid_project_id():
"""Test that empty project ID raises validation error."""
env_dict = {"GOOGLE_CLOUD_PROJECT": ""}
with pytest.raises(ValidationError):
ServerEnv.model_validate(env_dict)
What to Test
Behaviors
Test function outputs and state changes:
- Return values are correct
- State is modified as expected
- Side effects occur (logging, callbacks)
Error Conditions
Test invalid inputs and edge cases:
- Invalid parameter types
- Missing required values
- Boundary conditions (empty, max, negative)
Integration Points
Test connections between components:
- Callbacks are registered and invoked
- Tools are added to agent correctly
- Configuration flows through system
Mypy Override for Tests
Tests use relaxed type checking for pragmatism:
Still type hint conftest.py fully, but test functions can be more flexible.
Running Tests
# Full coverage report
uv run pytest --cov --cov-report=term-missing
# HTML report for detailed view
uv run pytest --cov --cov-report=html
open htmlcov/index.html
# Specific tests
uv run pytest tests/test_integration.py -v
uv run pytest tests/test_file.py::test_name -v
# Watch mode (requires pytest-watch)
uv run ptw
Examples
See tests/conftest.py and existing test files for complete patterns:
- Fixture factories
- ADK mocks
- Environment mocking
- Pydantic validation tests
- Async test patterns (with pytest-asyncio)