This document provides guidelines and best practices for writing and maintaining tests for the CLI Code project.
- Testing Structure
- Running Tests
- Mock Objects and API Interactions
- API Version Compatibility
- Lessons Learned
Tests are organized in two main directories:
test_dir/: Contains most test filestests/: Contains all test files, organized by module (e.g.,tests/models,tests/tools).
Test file naming follows these conventions:
- Basic test files:
test_<component>.py - Coverage-focused tests:
test_<component>_coverage.py - Tests for edge cases:
test_<component>_edge_cases.py - Advanced/comprehensive tests:
test_<component>_comprehensive.py
python -m pytestpython -m pytest --cov=src# Run tests in a specific file
python -m pytest tests/models/test_gemini.py
# Run a specific test
python -m pytest tests/models/test_gemini.py::test_generate_simple_text_responseWhen testing components that interact with external APIs (like Gemini or Ollama), proper mocking is essential. Here are some guidelines:
Use mocker.MagicMock() (provided by pytest-mock) instead of direct unittest.mock.MagicMock when creating mock objects:
# Preferred
mock_object = mocker.MagicMock()
# Avoid using spec unless necessary
# Avoid: mock_object = mock.MagicMock(spec=SomeClass)When mocking API response objects:
- Build mock objects hierarchically from inside out
- Set all necessary attributes explicitly
- Avoid using
__getattr__or other magic methods in mocks - For complex objects, create separate variables for each level to keep the code readable
Example:
# Create the innermost part
mock_response_part = mocker.MagicMock()
mock_response_part.text = "Hello, world"
mock_response_part.function_call = None
# Create the content object that contains parts
mock_content = mocker.MagicMock()
mock_content.parts = [mock_response_part]
mock_content.role = "model"
# Create the candidate object that contains content
mock_candidate = mocker.MagicMock()
mock_candidate.content = mock_content
mock_candidate.finish_reason = "STOP"
# Create the final response
mock_api_response = mocker.MagicMock()
mock_api_response.candidates = [mock_candidate]When mocking confirmation prompts (e.g., questionary.confirm):
# Create a mock object that has an .ask method
mock_confirm_obj = mocker.MagicMock()
mock_confirm_obj.ask.return_value = True # or False
mock_confirm = mocker.patch("path.to.questionary.confirm", return_value=mock_confirm_obj)External APIs evolve over time, which can break tests. Follow these practices to make tests more resilient:
- Use loose coupling to implementation details
- Avoid importing classes directly from unstable APIs when possible
- For required imports, use try/except blocks to handle missing imports
- Consider using conditional test execution with
@pytest.mark.skipif
Example of conditional imports:
try:
from google.generativeai.types.content_types import FunctionCallingMode as FunctionCall
IMPORTS_AVAILABLE = True
except ImportError:
IMPORTS_AVAILABLE = False
# Create mock class as fallback
class FunctionCall: pass
@pytest.mark.skipif(not IMPORTS_AVAILABLE, reason="Required imports not available")
def test_feature_requiring_imports():
# Test code hereRecent work with the Google Generative AI (Gemini) API highlighted several key lessons:
-
API Structure Evolution: The Gemini API structure has changed over time. Classes like
Candidate,Content, andFunctionCallhave moved between modules. -
Import Strategies:
- Import specifically from submodules rather than top-level packages
- Use alternative imports when direct imports aren't available:
# Instead of from google.generativeai.types import Candidate # Use from google.ai.generativelanguage_v1beta.types.generative_service import Candidate
-
Mock Object Limitations:
- Setting
__getattr__on mock objects isn't supported - Using
.speccan make mocks too restrictive - Mock objects directly with the attributes they need instead of trying to mimic class behavior exactly
- Setting
-
Test Assertions:
- Focus assertions on behavior, not implementation
- Verify key interactions rather than every intermediate step
- For error messages, match the message pattern rather than expecting exact strings
-
Questionary Mocking:
- Mocking
questionary.confirm()requires special attention since it returns an object with an.ask()method - Create a proper mock structure:
mock_confirm_obj.ask.return_value = True/False
- Mocking
-
Focus on Key Behaviors: Test that the core functionality works, not the implementation details.
-
Isolate External Dependencies: Always mock external dependencies to prevent tests from being impacted by API changes or availability.
-
Regular Updates: Update tests when APIs change, focusing on the behavior rather than the exact implementation.
-
Error Handling: Include proper error handling in tests to make them more robust against changes.