# testing-patterns > Testing conventions and patterns for Claude Usage Monitor - Author: Willie Beamin - Repository: ImWillieBeamin/cc-use-mon - Version: 20260131193143 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/ImWillieBeamin/cc-use-mon - Web: https://mule.run/skillshub/@@ImWillieBeamin/cc-use-mon~testing-patterns:20260131193143 --- --- name: testing-patterns description: Testing conventions and patterns for Claude Usage Monitor --- # Testing Patterns for Claude Usage Monitor This skill documents the testing conventions and patterns used in this project. ## Test Organization ### File Structure - `test_api.py` - API endpoint tests using Flask test client - `test_database.py` - Database operation unit tests - `test_parser.py` - JSONL parsing and cost calculation tests - `test_properties.py` - Property-based tests with Hypothesis - `test_error_paths.py` - Error handling and edge case tests - `conftest.py` - Shared fixtures and Hypothesis profiles ### Class Naming Use descriptive class names that indicate what's being tested: ```python class TestStatsEndpoint: """Tests for /api/stats endpoint.""" class TestAccountOperations: """Tests for account-related database operations.""" ``` ### Test Method Naming Use `test_action_condition_expected_result` pattern: ```python def test_get_stats_with_days_filter(self, client): def test_pagination_beyond_total_returns_empty(self, client): def test_insert_prompt_duplicate_ignored(self): ``` ## Assertion Patterns ### Always Include Context Every assertion should have a descriptive message explaining what went wrong: ```python # Good assert response.status_code == 200, \ f"GET /api/stats failed: expected 200, got {response.status_code}" # Bad assert response.status_code == 200 ``` ### Check Multiple Fields with Loops ```python required_fields = ['tokens', 'cache_hit_rate', 'total_cost'] for field in required_fields: assert field in data, \ f"Response missing field '{field}'. Got: {list(data.keys())}" ``` ### Verify Preconditions ```python sessions = client.get("/api/sessions").get_json() assert sessions, "No sessions found in test data" # Verify fixture worked session_id = sessions[0]['id'] ``` ## Fixture Patterns ### Use `clean_db` for Database Isolation ```python def test_something(self, clean_db): """Test with a fresh database (data cleared, schema intact).""" # Test code here ``` ### Use `sample_*` Fixtures for Common Test Data ```python @pytest.fixture def sample_prompts(sample_session): """Create sample prompts and return their IDs.""" # Returns list of prompt IDs for testing ``` ### Deterministic Timestamps Use fixed timestamps instead of `datetime.now()`: ```python BASE_TIME = datetime(2026, 1, 15, 12, 0, 0) def get_test_timestamp(offset_minutes=0): return (BASE_TIME - timedelta(minutes=offset_minutes)).isoformat() ``` ## Property-Based Testing ### When to Use Hypothesis - Testing functions with many valid inputs (validation, parsing) - Testing invariants that should hold for all inputs - Finding edge cases in numeric calculations ### Custom Strategies Define reusable strategies in the test file: ```python # Token counts: non-negative integers, reasonable maximum token_count = st.integers(min_value=0, max_value=10_000_000) # Model names from pricing dict model_name = st.sampled_from(list(PRICING.keys())) ``` ### Hypothesis Profiles - `default` - 100 examples, balanced - `quick` - 20 examples, fast iteration - `ci` - 500 examples, thorough CI testing - `debug` - 10 examples, verbose output Run with specific profile: ```bash HYPOTHESIS_PROFILE=quick pytest tests/test_properties.py ``` ### Suppress Health Checks When Needed ```python @settings(max_examples=200, suppress_health_check=[HealthCheck.filter_too_much]) def test_with_filters(self, value): assume(some_condition) # Filter values ``` ## Flask Test Client Patterns ### Request Context for Validation Tests ```python @pytest.fixture(autouse=True) def setup_flask_app(self): from flask import Flask self.app = Flask(__name__) def test_validation(self, value): with self.app.test_request_context(f'/?param={value}'): result = get_int_param('param') ``` ### Module-Scoped Fixtures for Expensive Setup ```python @pytest.fixture(scope="module") def app_client(): """Create test client with seeded database.""" # Expensive setup done once per module ``` ## Common Test Patterns ### Testing API Response Structure ```python def test_response_has_required_fields(self, client): response = client.get("/api/stats") data = response.get_json() required_fields = ['tokens', 'cache_hit_rate', 'total_cost'] for field in required_fields: assert field in data, \ f"Missing field '{field}'. Got: {list(data.keys())}" ``` ### Testing Pagination ```python def test_pagination_respects_limit(self, client): response = client.get("/api/prompts?page=1&limit=5") data = response.get_json() assert len(data['prompts']) <= 5, \ f"Pagination limit=5 not respected: got {len(data['prompts'])}" ``` ### Testing Error Paths ```python def test_invalid_id_returns_404(self, client): response = client.get("/api/prompts/99999") assert response.status_code == 404, \ f"Invalid ID should return 404, got {response.status_code}" ``` ### Testing Database Idempotency ```python def test_operation_is_idempotent(self): id1 = db.get_or_create_session("session-123", account_id, "/project") id2 = db.get_or_create_session("session-123", account_id, "/project") assert id1 == id2, \ f"Operation not idempotent: first={id1}, second={id2}" ``` ## Running Tests ### Quick Test Run ```bash pytest tests/ -x -q # Stop on first failure, quiet output ``` ### Verbose with Coverage ```bash pytest tests/ -v --cov=app --cov=parser ``` ### Run Specific Test Class ```bash pytest tests/test_api.py::TestStatsEndpoint -v ``` ### Property Tests with Statistics ```bash pytest tests/test_properties.py --hypothesis-show-statistics ```