Comprehensive testing guide for nui-python-shared-utils developers.
Last Updated: 2025-11-16
The package uses pytest as the testing framework with comprehensive test coverage across all modules. Tests are organized into categories using pytest markers for flexible test execution.
# Run all tests
pytest
# Run with coverage report
pytest --cov=nui_shared_utils --cov-report=html
# Run specific test categories
pytest -m unit # Unit tests only
pytest -m integration # Integration tests only
pytest -m slow # Long-running teststests/
├── conftest.py # Shared fixtures and configuration
├── test_config.py # Configuration system tests
├── test_secrets_helper.py # Secrets Manager integration tests
├── test_slack_client.py # Slack integration tests
├── test_slack_formatter.py # Slack formatting tests
├── test_es_client.py # Elasticsearch client tests
├── test_es_query_builder.py # Query builder tests
├── test_db_client.py # Database client tests
├── test_cloudwatch_metrics.py # CloudWatch metrics tests
├── test_error_handler.py # Error handling tests
├── test_timezone.py # Timezone utility tests
├── test_base_client.py # Base client tests
└── test_utils.py # Utility function tests
Fast, isolated tests that mock external dependencies.
Characteristics:
- No AWS service calls
- Use mocking extensively
- Fast execution (< 1 second per test)
- High coverage of code paths
Example:
@pytest.mark.unit
def test_config_defaults():
"""Test default configuration values."""
config = Config()
assert config.es_host == "localhost:9200"
assert config.aws_region == "us-east-1"Tests requiring actual AWS services or external dependencies.
Characteristics:
- May require AWS credentials
- Use
motofor AWS service mocking - Slower execution
- Test actual integration patterns
Example:
@pytest.mark.integration
def test_secrets_manager_integration():
"""Test actual Secrets Manager integration."""
# Requires AWS credentials or moto mock
secret = get_secret("test-secret")
assert secret is not NoneLong-running tests that may timeout or require special attention.
Example:
@pytest.mark.slow
def test_large_elasticsearch_query():
"""Test query with large result set."""
# ... slow operation# All tests with output
pytest -v
# Stop on first failure
pytest -x
# Run specific test file
pytest tests/test_slack_client.py
# Run specific test function
pytest tests/test_slack_client.py::test_send_message
# Run tests matching pattern
pytest -k "slack" # All tests with "slack" in name# Terminal coverage report
pytest --cov=nui_shared_utils
# HTML coverage report
pytest --cov=nui_shared_utils --cov-report=html
# Opens in: htmlcov/index.html
# Coverage with missing lines
pytest --cov=nui_shared_utils --cov-report=term-missing
# Fail if coverage below threshold
pytest --cov=nui_shared_utils --cov-fail-under=90# Unit tests only (fast)
pytest -m unit
# Skip integration tests
pytest -m "not integration"
# Skip slow tests
pytest -m "not slow"
# Unit tests excluding slow ones
pytest -m "unit and not slow"The package uses moto for mocking AWS services in tests:
import pytest
from moto import mock_secretsmanager
import boto3
@pytest.fixture
def mock_secrets():
"""Mock AWS Secrets Manager."""
with mock_secretsmanager():
client = boto3.client('secretsmanager', region_name='us-east-1')
client.create_secret(
Name='test-secret',
SecretString='{"key": "value"}'
)
yield client
def test_with_mock_secrets(mock_secrets):
"""Test using mocked secrets."""
from nui_shared_utils import get_secret
secret = get_secret('test-secret')
assert secret['key'] == 'value'# Set test environment variables
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
# Run tests
pytestThe pytest.ini file configures pytest behavior:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
unit: Unit tests (fast, no external dependencies)
integration: Integration tests (require AWS/external services)
slow: Slow-running testsimport pytest
from nui_shared_utils import SlackClient
class TestSlackClient:
"""Test suite for SlackClient."""
@pytest.fixture
def mock_slack_config(self):
"""Fixture for Slack configuration."""
return {
'bot_token': 'xoxb-test-token',
'webhook_url': 'https://hooks.slack.com/test'
}
@pytest.mark.unit
def test_initialization(self, mock_slack_config):
"""Test client initialization."""
# Arrange
config = mock_slack_config
# Act
client = SlackClient(bot_token=config['bot_token'])
# Assert
assert client.bot_token == config['bot_token']
@pytest.mark.unit
def test_message_formatting(self):
"""Test message formatting."""
# Test implementation
passfrom unittest.mock import Mock, patch
@pytest.mark.unit
def test_elasticsearch_search():
"""Test Elasticsearch search with mocking."""
with patch('elasticsearch.Elasticsearch') as mock_es:
# Configure mock
mock_es.return_value.search.return_value = {
'hits': {'hits': [{'_source': {'data': 'test'}}]}
}
# Test code
from nui_shared_utils import ElasticsearchClient
client = ElasticsearchClient()
results = client.search(index='test', body={})
# Assertions
assert len(results['hits']['hits']) == 1
mock_es.return_value.search.assert_called_once()@pytest.mark.unit
def test_error_retry_logic():
"""Test retry decorator behavior."""
from nui_shared_utils import with_retry
call_count = 0
@with_retry(max_attempts=3)
def failing_function():
nonlocal call_count
call_count += 1
raise Exception("Test error")
with pytest.raises(Exception):
failing_function()
assert call_count == 3 # Should retry 3 times@pytest.mark.parametrize("input_value,expected", [
("test", "TEST"),
("hello", "HELLO"),
("", ""),
])
def test_uppercase_conversion(input_value, expected):
"""Test uppercase conversion with multiple inputs."""
result = input_value.upper()
assert result == expected- utils.py: 100% coverage
- base_client.py: 94.52% coverage
- config.py: 100% coverage
- Overall Target: 90%+ coverage
# Generate coverage report
pytest --cov=nui_shared_utils --cov-report=term-missing
# View detailed HTML report
pytest --cov=nui_shared_utils --cov-report=html
open htmlcov/index.html # Mac
xdg-open htmlcov/index.html # LinuxWhen adding new code:
- Write tests before implementation (TDD)
- Aim for 90%+ coverage on new modules
- Include edge cases and error paths
- Test both success and failure scenarios
The project uses GitHub Actions for automated testing:
# .github/workflows/test.yml
- Run tests with coverage
- Upload coverage reports
- Fail if coverage drops below threshold# Simulate CI environment locally
pytest --cov=nui_shared_utils --cov-fail-under=90 -vError:
NoCredentialsError: Unable to locate credentials
Solution:
# Set dummy credentials for testing
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1Error:
ImportError: No module named 'elasticsearch'
Solution:
# Install dev dependencies
pip install -e .[dev]Error:
fixture 'mock_config' not found
Solution:
- Check
conftest.pyfor fixture definitions - Ensure fixture scope is correct
- Verify fixture name matches usage
# Run with verbose output
pytest -v
# Show print statements
pytest -s
# Drop into debugger on failure
pytest --pdb
# Show local variables on failure
pytest -l
# Increase verbosity
pytest -vv- One test file per module -
test_slack_client.pyforslack_client.py - Group related tests - Use test classes for logical grouping
- Clear test names -
test_send_message_with_valid_token() - Use fixtures - Share setup code via fixtures
- Test one thing - Each test should verify one behavior
- Arrange-Act-Assert - Clear test structure
- No test interdependencies - Tests should run independently
- Mock external services - Don't rely on external APIs
- Test edge cases - Empty inputs, None values, errors
- Update tests with code changes - Keep tests in sync
- Remove obsolete tests - Clean up when refactoring
- Document complex tests - Explain why, not just what
- Keep tests fast - Use mocking to avoid slow operations
For general contribution guidelines, see CONTRIBUTING.md