Comprehensive testing with unit, integration, and E2E tests for the modular cli_audit package.
The test suite validates all 21 modules across Phase 1 (Detection & Auditing) and Phase 2 (Installation & Upgrade Management).
Test organization:
tests/
├── test_bulk.py # Bulk operations
├── test_catalog_and_collectors.py # Catalog and version collectors
├── test_config.py # Configuration management
├── test_environment.py # Environment detection
├── test_install_plan.py # Planning & dependencies
├── test_installer.py # Installation logic
├── test_local_state.py # Local state management
├── test_logging.py # Logging configuration
├── test_package_managers.py # Package manager abstractions
├── test_prerequisites.py # Prerequisite resolution
├── test_reconcile.py # Duplicate cleanup
├── test_update_fixes.py # cmd_update_local / multi-version merge regressions
├── test_upgrade.py # Upgrade workflows
├── test_upstream_cache.py # Upstream cache management
└── integration/
└── test_e2e_install.py # End-to-end installation tests
Test coverage:
- 14 unit test files covering all 21 modules
- 1 integration test directory
- Fixtures for mocking external services
Requirements:
# Python 3.14+ with uv package manager
python3 --version # 3.14 required
uv --version # uv required
# Sync all dependencies (including dev)
uv sync --extra devTest dependencies (from pyproject.toml):
- pytest — Test framework
- pytest-cov — Coverage reporting
- pytest-mock — Mocking utilities
- pytest-xdist — Parallel test execution
Test environment variables:
CLI_AUDIT_DEBUG=1 # Enable debug logging in tests
CLI_AUDIT_OFFLINE=1 # Force offline mode (no network calls)
CLI_AUDIT_TIMEOUT_SECONDS=1 # Fast timeout for testsRun all tests (use uv run):
# From project root - ALWAYS use uv run
uv run python -m pytest
# Verbose output
uv run python -m pytest -v
# With coverage
uv run python -m pytest --cov=cli_audit --cov-report=html
# Parallel execution (fast)
uv run python -m pytest -n autoRun specific test files:
# Single test file
uv run python -m pytest tests/test_config.py -v
# Single test class
uv run python -m pytest tests/test_config.py::TestToolConfig -v
# Single test function
uv run python -m pytest tests/test_config.py::TestToolConfig::test_tool_config_defaults -vRun integration tests:
uv run python -m pytest tests/integration/ -vSmoke test (quick validation):
./scripts/test_smoke.shFilter tests by marker:
# Run only slow tests
uv run python -m pytest -m slow
# Skip slow tests
uv run python -m pytest -m "not slow"
# Run only unit tests
uv run python -m pytest -m unitTest naming:
- Test files:
test_<module>.py - Test classes:
Test<Component> - Test functions:
test_<behavior> - Use descriptive names:
test_config_invalid_timeout_too_low
Test structure (Arrange-Act-Assert):
def test_tool_config_defaults():
# Arrange
config = ToolConfig()
# Act
result = config.get_install_method()
# Assert
assert result == "auto"Fixture patterns:
import pytest
@pytest.fixture
def sample_tool():
"""Provide a sample Tool instance for tests."""
return Tool("ripgrep", ("rg",), "gh", ("BurntSushi", "ripgrep"))
def test_tool_detection(sample_tool):
"""Test tool detection with fixture."""
assert sample_tool.name == "ripgrep"Mocking external calls:
from unittest.mock import patch, MagicMock
@patch('cli_audit.collectors.collect_github')
def test_version_collection(mock_collect):
"""Mock GitHub API calls."""
mock_collect.return_value = ("v1.2.3", "1.2.3")
tag, version = collect_github("owner", "repo")
assert version == "1.2.3"
mock_collect.assert_called_once()Test isolation:
- Tests should not depend on each other
- Use fixtures for shared setup
- Clean up resources (files, processes) in teardown
- Mock external services (no real API calls in unit tests)
File system safety:
import tempfile
from pathlib import Path
def test_file_operations(tmp_path: Path):
"""Use pytest tmp_path fixture for safe file operations."""
test_file = tmp_path / "test.json"
test_file.write_text('{"key": "value"}')
# Test file operations
assert test_file.exists()
# No cleanup needed, tmp_path auto-cleanedNetwork isolation:
# Mock network calls in unit tests
@patch('urllib.request.urlopen')
def test_http_fetch(mock_urlopen):
"""No real network calls in unit tests."""
mock_response = MagicMock()
mock_response.read.return_value = b'{"version": "1.2.3"}'
mock_urlopen.return_value = mock_response
# Test proceeds without real networkSubprocess safety:
@patch('subprocess.run')
def test_tool_execution(mock_run):
"""Mock subprocess calls."""
mock_run.return_value = MagicMock(
stdout="ripgrep 15.1.0",
returncode=0
)
# Test without executing real commandsBefore committing test changes:
- All tests pass:
uv run python -m pytestsucceeds - No skipped tests: Fix or document skipped tests
- Coverage maintained: New code has corresponding tests
- Isolation: Tests don't depend on execution order
- Fast execution: Unit tests complete in <5s total
- Descriptive names: Test names clearly describe what's tested
- Mocking: External services/network mocked appropriately
- Cleanup: Temporary files/resources cleaned up
- Documentation: Complex test logic has comments
Integration test checklist:
- E2E tests pass:
uv run python -m pytest tests/integration/ -v - Real environment tested: Not just mocks
- Idempotent: Can run multiple times safely
- Reasonable timeout: Integration tests <30s each
Good: Descriptive test names
def test_config_invalid_timeout_too_low():
"""Test that timeout < 1 raises ValueError."""
with pytest.raises(ValueError, match="timeout must be >= 1"):
Preferences(timeout_seconds=0)Bad: Vague test names
def test_config(): # ❌ What about config?
assert TrueGood: Arrange-Act-Assert structure
def test_tool_catalog_get():
# Arrange
catalog = ToolCatalog()
# Act
entry = catalog.get("ripgrep")
# Assert
assert entry is not None
assert entry.name == "ripgrep"Good: Use fixtures for shared setup
@pytest.fixture
def catalog():
"""Provide ToolCatalog instance."""
return ToolCatalog()
def test_catalog_has(catalog):
assert catalog.has("ripgrep")
def test_catalog_get(catalog):
assert catalog.get("ripgrep") is not NoneBad: Repeated setup in each test
def test_catalog_has():
catalog = ToolCatalog() # ❌ Repeated
assert catalog.has("ripgrep")
def test_catalog_get():
catalog = ToolCatalog() # ❌ Repeated
assert catalog.get("ripgrep") is not NoneGood: Mock external dependencies
@patch('cli_audit.collectors.collect_github')
def test_github_rate_limit(mock_collect):
mock_collect.side_effect = Exception("Rate limit exceeded")
with pytest.raises(Exception, match="Rate limit"):
collect_github("owner", "repo")Bad: Real API calls in unit tests
def test_github_collection():
# ❌ Real network call, slow and flaky
tag, version = collect_github("owner", "repo")
assert versionGood: Parametrized tests for multiple cases
@pytest.mark.parametrize("timeout,expected", [
(1, 1),
(5, 5),
(100, 100),
])
def test_timeout_valid_values(timeout, expected):
prefs = Preferences(timeout_seconds=timeout)
assert prefs.timeout_seconds == expectedGood: Use tmp_path for file operations
def test_snapshot_write(tmp_path):
snapshot_file = tmp_path / "test_snapshot.json"
data = {"tools": []}
write_snapshot(data, str(snapshot_file))
assert snapshot_file.exists()
loaded = json.loads(snapshot_file.read_text())
assert loaded == dataResources:
- Testing Guide:
docs/TESTING.md— Comprehensive testing strategies - Developer Guide:
docs/DEVELOPER_GUIDE.md— Contribution guidelines - Error Catalog:
docs/ERROR_CATALOG.md— Common error patterns
Debugging failing tests:
# Run with verbose output
uv run python -m pytest tests/test_config.py -vv
# Show print statements
uv run python -m pytest tests/test_config.py -s
# Stop on first failure
uv run python -m pytest -x
# Enter debugger on failure
uv run python -m pytest --pdb
# Show last failed tests
uv run python -m pytest --lfCommon issues:
- Import errors: Run
uv sync --extra devfirst, ensure uv run is used - Fixture not found: Check fixture scope and location
- Test order dependency: Tests should be independent, fix test isolation
- Slow tests: Mock external calls, use tmp_path, check for real network/subprocess
- Flaky tests: Usually timing issues, mock time-dependent operations
- VIRTUAL_ENV warning: Run
deactivatefirst if another venv is active
Test coverage:
# Generate coverage report
uv run python -m pytest --cov=cli_audit --cov-report=html
# View report
open htmlcov/index.html
# Show missing lines
uv run python -m pytest --cov=cli_audit --cov-report=term-missingTest-specific overrides:
-
Test discovery: pytest auto-discovers
test_*.pyfiles- All test files must start with
test_ - All test functions must start with
test_
- All test files must start with
-
Markers: Use pytest markers for test categorization
@pytest.mark.slow def test_full_integration(): pass @pytest.mark.unit def test_config_defaults(): pass
-
Fixtures: Prefer fixtures over setup/teardown
- Use
conftest.pyfor shared fixtures - Use
tmp_pathfor file operations - Use
monkeypatchfor environment variables
- Use
-
Coverage target: Aim for ≥80% coverage
- New modules should have ≥80% test coverage
- Critical paths (detection, installation) should have ≥90%
- 100% not required, focus on meaningful tests
-
Test speed: Keep unit tests fast
- Unit tests: <5s total
- Integration tests: <30s total
- Mock slow operations (network, subprocess)