Testing¶
This guide covers GIANT's testing practices and conventions.
Test Organization¶
tests/
├── conftest.py # Shared fixtures
├── unit/ # Unit tests
│ ├── agent/
│ ├── llm/
│ ├── core/
│ ├── geometry/
│ ├── eval/
│ └── cli/
└── integration/ # Integration tests
├── wsi/
└── llm/
Running Tests¶
Unit Tests (Fast)¶
# All unit tests
pytest tests/unit
# Stop on first failure
pytest tests/unit -x
# Specific module
pytest tests/unit/llm/
# Single test file
pytest tests/unit/llm/test_openai_client.py
# Single test
pytest tests/unit/llm/test_openai_client.py::test_generate_response
With Coverage¶
# Generate coverage report
pytest tests/unit --cov=giant --cov-report=term-missing
# HTML coverage report
pytest tests/unit --cov=giant --cov-report=html
open htmlcov/index.html
Integration Tests¶
# Requires real WSI files
pytest tests/integration/wsi/
# Requires API keys (costs money!)
GIANT_RUN_LIVE_TESTS=1 pytest tests/integration/llm/
Test Markers¶
Available Markers¶
| Marker | Description | Default |
|---|---|---|
@pytest.mark.cost |
Requires live API, costs money | Skipped |
@pytest.mark.integration |
Requires real resources | Included |
@pytest.mark.live |
Requires live external service | Skipped |
Usage¶
import pytest
@pytest.mark.cost
async def test_real_api_call():
"""This test calls a real API and costs money."""
...
@pytest.mark.integration
def test_with_real_wsi():
"""This test requires real WSI files."""
...
Running Marked Tests¶
# Skip cost tests (default)
pytest tests/
# Include cost tests (requires GIANT_RUN_LIVE_TESTS=1)
GIANT_RUN_LIVE_TESTS=1 pytest -m cost
# Only integration tests
pytest -m integration
# Exclude integration tests
pytest -m "not integration"
Writing Tests¶
Test Structure¶
"""Tests for giant.module.feature module."""
import pytest
from giant.module.feature import FeatureClass
class TestFeatureClass:
"""Tests for FeatureClass."""
def test_basic_functionality(self) -> None:
"""Test that basic feature works correctly."""
# Arrange
obj = FeatureClass(name="test")
# Act
result = obj.process()
# Assert
assert result == "expected"
def test_raises_on_invalid_input(self) -> None:
"""Test that invalid input raises ValueError."""
obj = FeatureClass(name="")
with pytest.raises(ValueError) as exc_info:
obj.process()
assert "empty" in str(exc_info.value)
Async Tests¶
import pytest
@pytest.mark.asyncio
async def test_async_function() -> None:
"""Test async function."""
result = await async_function()
assert result is not None
Parametrized Tests¶
import pytest
@pytest.mark.parametrize("input,expected", [
("case1", "result1"),
("case2", "result2"),
("case3", "result3"),
])
def test_multiple_cases(input: str, expected: str) -> None:
"""Test multiple input cases."""
assert process(input) == expected
Fixtures¶
Shared Fixtures (conftest.py)¶
# tests/conftest.py
import pytest
from giant.geometry import Region
@pytest.fixture
def sample_region() -> Region:
"""Create a sample Region for testing."""
return Region(x=100, y=200, width=50, height=50)
@pytest.fixture
def mock_wsi_path(tmp_path):
"""Create a mock WSI path."""
wsi = tmp_path / "test.svs"
wsi.touch()
return wsi
Module-Specific Fixtures¶
# tests/unit/llm/conftest.py
import pytest
from unittest.mock import MagicMock, AsyncMock
from giant.llm.protocol import LLMResponse, StepResponse
@pytest.fixture
def mock_llm_response() -> LLMResponse:
"""Create a mock LLM response."""
return LLMResponse(
step_response=StepResponse(
reasoning="Test reasoning",
action={"action_type": "answer", "answer_text": "Test answer"}
),
usage=TokenUsage(
prompt_tokens=100,
completion_tokens=50,
total_tokens=150,
cost_usd=0.01,
),
model="gpt-5.2",
latency_ms=500.0,
)
Mocking¶
Basic Mocking¶
from unittest.mock import MagicMock, patch
def test_with_mock():
mock_client = MagicMock()
mock_client.call.return_value = "result"
result = function_under_test(client=mock_client)
mock_client.call.assert_called_once()
assert result == "result"
Async Mocking¶
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_async_with_mock():
mock_provider = MagicMock()
mock_provider.generate_response = AsyncMock(return_value=mock_response)
result = await agent.run(provider=mock_provider)
mock_provider.generate_response.assert_called()
Patching¶
from unittest.mock import patch
def test_with_patch():
with patch("giant.module.external_function") as mock_func:
mock_func.return_value = "mocked"
result = function_that_calls_external()
assert result == "mocked"
Using pytest-mock¶
def test_with_mocker(mocker):
mock_func = mocker.patch("giant.module.function")
mock_func.return_value = "mocked"
result = function_under_test()
assert result == "mocked"
Test Coverage¶
Coverage Requirements¶
- Minimum: 90% (enforced in CI)
- Target: 95%+
Checking Coverage¶
# Terminal report
pytest tests/unit --cov=giant --cov-report=term-missing
# HTML report
pytest tests/unit --cov=giant --cov-report=html
# Fail if below threshold
pytest tests/unit --cov=giant --cov-fail-under=90
Ignoring Coverage¶
For code that can't be tested:
Test Data¶
Sample WSI Files¶
For integration tests, download OpenSlide test data:
mkdir -p tests/integration/wsi/data
curl -L -o tests/integration/wsi/data/CMU-1-Small-Region.svs \
https://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1-Small-Region.svs
Using Test Data¶
import pytest
from pathlib import Path
@pytest.fixture
def test_wsi_path() -> Path:
"""Path to test WSI file."""
path = Path("tests/integration/wsi/data/CMU-1-Small-Region.svs")
if not path.exists():
pytest.skip("Test WSI not available")
return path
Common Patterns¶
Testing Exceptions¶
def test_raises_on_error():
with pytest.raises(ValueError) as exc_info:
function_that_raises()
assert "specific message" in str(exc_info.value)
Testing Warnings¶
Temporary Files¶
def test_with_temp_file(tmp_path):
test_file = tmp_path / "test.json"
test_file.write_text('{"key": "value"}')
result = load_json(test_file)
assert result["key"] == "value"
CI Integration¶
Tests run automatically on:
- Pull requests
- Pushes to main/dev branches
CI configuration enforces:
- All unit tests pass
- Coverage >= 90%
- No linting errors
- Type checking passes
Troubleshooting¶
Test Not Found¶
# Ensure correct path
pytest tests/unit/module/test_file.py -v
# Check for import errors
python -c "import tests.unit.module.test_file"
Fixture Not Found¶
Async Test Issues¶
# Ensure pytest-asyncio is installed
pip install pytest-asyncio
# Check marker is present
@pytest.mark.asyncio
async def test_async(): ...