Testing Guide¶
easy_sm has a comprehensive test suite covering all commands and core modules.
Test Suite Overview¶
Total: 120 tests across 8 test files, covering all commands and core modules.
All tests use mocked external dependencies (subprocess, boto3, SageMaker SDK) for fast, reliable execution without requiring AWS credentials or Docker.
Running Tests¶
Run All Tests¶
Run Specific Test File¶
pytest tests/test_build_command.py
pytest tests/test_cloud_commands.py
pytest tests/test_init_command.py
pytest tests/test_local_commands.py
pytest tests/test_push_command.py
pytest tests/test_update_command.py
pytest tests/test_config.py
pytest tests/test_helpers.py
Run Specific Test Function¶
pytest tests/test_build_command.py::test_build_with_custom_tag
pytest tests/test_cloud_commands.py::test_train_command
Run Tests with Coverage¶
Example output:
tests/test_build_command.py ............ [20%]
tests/test_cloud_commands.py .......... [50%]
tests/test_config.py ............ [75%]
tests/test_helpers.py ............ [100%]
====== 120 passed in 2.34s ======
---------- coverage: ----------
Name Stmts Miss Cover
-------------------------------------------------------
easy_sm/__init__.py 1 0 100%
easy_sm/__main__.py 45 0 100%
easy_sm/commands/build.py 67 0 100%
...
-------------------------------------------------------
TOTAL 1234 12 99%
Run Tests with Verbose Output¶
Run Tests Matching Pattern¶
# Run all tests with "deploy" in the name
pytest -k deploy
# Run all tests except slow ones
pytest -k "not slow"
Test Files¶
Command Tests (87 tests)¶
test_init_command.py (7 tests)¶
Tests project initialization with various configurations:
test_init_default_values- Initialize with default configurationtest_init_custom_values- Initialize with custom valuestest_init_creates_directory_structure- Verify directory creationtest_init_creates_config_file- Verify config file creationtest_init_with_invalid_python_version- Test validationtest_init_with_existing_project- Test overwrite behaviortest_init_with_missing_requirements- Test requirements handling
test_build_command.py (13 tests)¶
Tests Docker image building with parameter variations and error scenarios:
test_build_with_default_tag- Build with latest tagtest_build_with_custom_tag- Build with specific tagtest_build_with_auto_detected_app_name- Auto-detect app nametest_build_with_explicit_app_name- Override app nametest_build_with_missing_config- Handle missing configurationtest_build_with_invalid_dockerfile- Handle Docker errorstest_build_with_custom_dockerfile_path- Custom Dockerfile locationtest_build_caching- Verify build caching workstest_build_with_build_args- Pass build argumentstest_build_output_parsing- Parse Docker build outputtest_build_with_no_cache_flag- Disable build cachetest_build_failure_cleanup- Clean up on failuretest_build_with_progress_display- Progress indication
test_local_commands.py (23 tests)¶
Tests local training, deployment, processing, and stop commands:
test_local_train_with_mock_subprocess- Train model locally with mocked subprocesstest_local_train_config_loading- Config file loadingtest_local_train_missing_config- Handle missing configtest_local_train_custom_docker_tag- Use custom Docker tagtest_local_train_command_structure- Verify command structuretest_local_deploy_with_mock_subprocess- Deploy model locallytest_local_deploy_custom_port- Use custom porttest_local_deploy_port_long_option- Long-form port optiontest_local_stop_with_mock_subprocess- Stop local deploymenttest_local_stop_with_custom_port- Stop with custom port- 13 more tests covering integration workflows, config validation, and script permissions
test_cloud_commands.py (28 tests)¶
Tests SageMaker operations:
Training: - test_train_success - Basic training job - test_train_with_custom_docker_tag - Use custom Docker tag - test_train_with_multiple_instances - Distributed training - test_list_training_jobs_success - List all jobs - test_list_training_jobs_names_only - Names-only output - test_get_model_artifacts_success - Get model S3 path
Deployment: - test_deploy_provisioned - Deploy provisioned endpoint - test_deploy_with_multiple_instances - Multi-instance deployment - test_deploy_serverless - Deploy serverless endpoint - test_deploy_with_tags - Add resource tags
Processing: - test_process_job - Run processing job - test_batch_transform - Batch predictions - test_batch_transform_with_multiple_files - Process multiple files
Management: - test_list_endpoints - List all endpoints - test_delete_endpoint - Delete endpoint - test_delete_endpoint_with_config - Delete endpoint and config - 11 more tests...
test_push_command.py (9 tests)¶
Tests ECR image push with authentication:
test_push_with_profile_auth- Push with AWS profiletest_push_with_iam_role_auth- Push with IAM roletest_push_with_external_id- Push with external IDtest_push_creates_ecr_repository- Auto-create repositorytest_push_with_existing_repository- Use existing repositorytest_push_with_custom_tag- Push specific tagtest_push_authentication_failure- Handle auth errorstest_push_with_cross_account- Cross-account pushtest_push_output_format- Verify output format
test_update_command.py (7 tests)¶
Tests shell script update command:
test_update_scripts_basic- Update all scriptstest_update_scripts_auto_detect_app- Auto-detect app nametest_update_scripts_with_customizations- Preserve customizations warningtest_update_scripts_missing_app_directory- Handle missing directorytest_update_scripts_file_permissions- Verify correct permissions (0o755)test_update_scripts_security_fixes- Verify proper quotingtest_update_scripts_dry_run- Dry-run mode
Module Tests (33 tests)¶
test_config.py (16 tests)¶
Tests configuration loading, saving, serialization:
test_config_creation- Create Config objecttest_config_to_dict- Serialize to dictionarytest_config_from_dict- Deserialize from dictionarytest_config_manager_load- Load from JSON filetest_config_manager_save- Save to JSON filetest_config_manager_get_or_create- Get or create defaulttest_config_manager_missing_file- Handle missing filetest_config_manager_invalid_json- Handle malformed JSONtest_config_validation_empty_fields- Validate required fieldstest_config_validation_invalid_types- Type checkingtest_config_with_defaults- Default values- 5 more tests...
test_helpers.py (17 tests)¶
Tests subprocess execution, output handling, error propagation:
test_safe_run_subprocess_success- Successful executiontest_safe_run_subprocess_failure- Handle process errorstest_safe_run_subprocess_timeout- Handle timeoutstest_safe_run_subprocess_output_capture- Capture stdout/stderrtest_auto_detect_app_name_single_config- Auto-detect single configtest_auto_detect_app_name_multiple_configs- Fail on multiple configstest_auto_detect_app_name_no_config- Fail on no configtest_get_app_name_explicit- Use explicit app nametest_get_app_name_auto_detect- Auto-detect app nametest_get_iam_role_from_param- Use explicit IAM roletest_get_iam_role_from_env_var- Use SAGEMAKER_ROLE env vartest_get_iam_role_missing- Error when not providedtest_load_config- Load configuration- 4 more tests...
Testing Philosophy¶
Unit Tests¶
Each test focuses on a single unit of functionality:
def test_build_with_custom_tag():
"""Test building Docker image with custom tag."""
# Arrange
app_name = "my-app"
docker_tag = "v1.0"
# Act
result = build_command(app_name, docker_tag)
# Assert
assert result.success
assert result.image_name == f"my-app:{docker_tag}"
Mocked Dependencies¶
External dependencies are mocked for isolation:
@mock.patch('subprocess.run')
@mock.patch('boto3.client')
def test_train_command(mock_boto, mock_subprocess):
"""Test training command with mocked AWS calls."""
# Setup mocks
mock_sagemaker = mock.Mock()
mock_boto.return_value = mock_sagemaker
# Run command
result = train_command(...)
# Verify AWS calls
mock_sagemaker.create_training_job.assert_called_once()
Fast Execution¶
No actual AWS calls or Docker builds:
Test Coverage¶
Aim for >95% code coverage:
Code Quality Tools¶
Type Checking¶
Run mypy for type checking:
Linting and Formatting¶
Use ruff for linting and formatting:
# Check for issues
ruff check easy_sm/
# Fix automatically
ruff check --fix easy_sm/
# Format code
ruff format easy_sm/
Pre-Commit Checks¶
Before committing:
Writing Tests¶
Test Structure¶
Follow the Arrange-Act-Assert pattern:
def test_my_feature():
"""Test description."""
# Arrange - Set up test data
app_name = "test-app"
config = Config(...)
# Act - Execute functionality
result = my_function(app_name, config)
# Assert - Verify results
assert result.success
assert result.value == expected_value
Mocking External Calls¶
Mock subprocess and AWS calls:
import mock
@mock.patch('subprocess.run')
def test_with_subprocess(mock_run):
"""Test function that calls subprocess."""
# Configure mock
mock_run.return_value = mock.Mock(
returncode=0,
stdout="Success",
stderr=""
)
# Run test
result = my_function()
# Verify subprocess was called correctly
mock_run.assert_called_with(
["docker", "build", ...],
check=True
)
Testing Error Cases¶
Test both success and failure paths:
def test_function_with_invalid_input():
"""Test function rejects invalid input."""
with pytest.raises(ValueError, match="Invalid app name"):
my_function("../../../etc/passwd")
Continuous Integration¶
Tests run automatically on:
- Pull requests
- Commits to main branch
- Tagged releases
GitHub Actions workflow:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.13'
- run: pip install -e . -r base-requirements.txt
- run: pytest --cov=easy_sm
- run: mypy easy_sm/
- run: ruff check easy_sm/
See Also¶
- Architecture - System design
- Code Style - Coding conventions
- Contributing - Development workflow