Files
everything-claude-code/.kiro/skills/python-testing/SKILL.md
Himanshu Sharma 535120d6b1 Add Kiro skills (18 SKILL.md files) (#811)
Co-authored-by: Sungmin Hong <hsungmin@amazon.com>
2026-03-22 21:55:45 -07:00

498 lines
11 KiB
Markdown

---
name: python-testing
description: >
Python testing best practices using pytest including fixtures, parametrization,
mocking, coverage analysis, async testing, and test organization. Use when
writing or improving Python tests.
metadata:
origin: ECC
globs: ["**/*.py", "**/*.pyi"]
---
# Python Testing
> This skill provides comprehensive Python testing patterns using pytest as the primary testing framework.
## Testing Framework
Use **pytest** as the testing framework for its powerful features and clean syntax.
### Basic Test Structure
```python
def test_user_creation():
"""Test that a user can be created with valid data"""
user = User(name="Alice", email="alice@example.com")
assert user.name == "Alice"
assert user.email == "alice@example.com"
assert user.is_active is True
```
### Test Discovery
pytest automatically discovers tests following these conventions:
- Files: `test_*.py` or `*_test.py`
- Functions: `test_*`
- Classes: `Test*` (without `__init__`)
- Methods: `test_*`
## Fixtures
Fixtures provide reusable test setup and teardown:
```python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def db_session():
"""Provide a database session for tests"""
engine = create_engine("sqlite:///:memory:")
Session = sessionmaker(bind=engine)
session = Session()
# Setup
Base.metadata.create_all(engine)
yield session
# Teardown
session.close()
def test_user_repository(db_session):
"""Test using the db_session fixture"""
repo = UserRepository(db_session)
user = repo.create(name="Alice", email="alice@example.com")
assert user.id is not None
```
### Fixture Scopes
```python
@pytest.fixture(scope="function") # Default: per test
def user():
return User(name="Alice")
@pytest.fixture(scope="class") # Per test class
def database():
db = Database()
db.connect()
yield db
db.disconnect()
@pytest.fixture(scope="module") # Per module
def app():
return create_app()
@pytest.fixture(scope="session") # Once per test session
def config():
return load_config()
```
### Fixture Dependencies
```python
@pytest.fixture
def database():
db = Database()
db.connect()
yield db
db.disconnect()
@pytest.fixture
def user_repository(database):
"""Fixture that depends on database fixture"""
return UserRepository(database)
def test_create_user(user_repository):
user = user_repository.create(name="Alice")
assert user.id is not None
```
## Parametrization
Test multiple inputs with `@pytest.mark.parametrize`:
```python
import pytest
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("invalid-email", False),
("", False),
("user@", False),
("@example.com", False),
])
def test_email_validation(email, expected):
result = validate_email(email)
assert result == expected
```
### Multiple Parameters
```python
@pytest.mark.parametrize("name,age,valid", [
("Alice", 25, True),
("Bob", 17, False),
("", 25, False),
("Charlie", -1, False),
])
def test_user_validation(name, age, valid):
result = validate_user(name, age)
assert result == valid
```
### Parametrize with IDs
```python
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
], ids=["lowercase", "another_lowercase"])
def test_uppercase(input, expected):
assert input.upper() == expected
```
## Test Markers
Use markers for test categorization and selective execution:
```python
import pytest
@pytest.mark.unit
def test_calculate_total():
"""Fast unit test"""
assert calculate_total([1, 2, 3]) == 6
@pytest.mark.integration
def test_database_connection():
"""Slower integration test"""
db = Database()
assert db.connect() is True
@pytest.mark.slow
def test_large_dataset():
"""Very slow test"""
process_million_records()
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+")
def test_new_syntax():
pass
```
**Run specific markers:**
```bash
pytest -m unit # Run only unit tests
pytest -m "not slow" # Skip slow tests
pytest -m "unit or integration" # Run unit OR integration
```
## Mocking
### Using unittest.mock
```python
from unittest.mock import Mock, patch, MagicMock
def test_user_service_with_mock():
"""Test with mock repository"""
mock_repo = Mock()
mock_repo.find_by_id.return_value = User(id="1", name="Alice")
service = UserService(mock_repo)
user = service.get_user("1")
assert user.name == "Alice"
mock_repo.find_by_id.assert_called_once_with("1")
@patch('myapp.services.EmailService')
def test_send_notification(mock_email_service):
"""Test with patched dependency"""
service = NotificationService()
service.send("user@example.com", "Hello")
mock_email_service.send.assert_called_once()
```
### pytest-mock Plugin
```python
def test_with_mocker(mocker):
"""Using pytest-mock plugin"""
mock_repo = mocker.Mock()
mock_repo.find_by_id.return_value = User(id="1", name="Alice")
service = UserService(mock_repo)
user = service.get_user("1")
assert user.name == "Alice"
```
## Coverage Analysis
### Basic Coverage
```bash
pytest --cov=src --cov-report=term-missing
```
### HTML Coverage Report
```bash
pytest --cov=src --cov-report=html
open htmlcov/index.html
```
### Coverage Configuration
```ini
# pytest.ini or pyproject.toml
[tool.pytest.ini_options]
addopts = """
--cov=src
--cov-report=term-missing
--cov-report=html
--cov-fail-under=80
"""
```
### Branch Coverage
```bash
pytest --cov=src --cov-branch
```
## Async Testing
### Testing Async Functions
```python
import pytest
@pytest.mark.asyncio
async def test_async_fetch_user():
"""Test async function"""
user = await fetch_user("1")
assert user.name == "Alice"
@pytest.fixture
async def async_client():
"""Async fixture"""
client = AsyncClient()
await client.connect()
yield client
await client.disconnect()
@pytest.mark.asyncio
async def test_with_async_fixture(async_client):
result = await async_client.get("/users/1")
assert result.status == 200
```
## Test Organization
### Directory Structure
```
tests/
├── unit/
│ ├── test_models.py
│ ├── test_services.py
│ └── test_utils.py
├── integration/
│ ├── test_database.py
│ └── test_api.py
├── conftest.py # Shared fixtures
└── pytest.ini # Configuration
```
### conftest.py
```python
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def app():
"""Application fixture available to all tests"""
return create_app()
@pytest.fixture
def client(app):
"""Test client fixture"""
return app.test_client()
def pytest_configure(config):
"""Register custom markers"""
config.addinivalue_line("markers", "unit: Unit tests")
config.addinivalue_line("markers", "integration: Integration tests")
config.addinivalue_line("markers", "slow: Slow tests")
```
## Assertions
### Basic Assertions
```python
def test_assertions():
assert value == expected
assert value != other
assert value > 0
assert value in collection
assert isinstance(value, str)
```
### pytest Assertions with Better Error Messages
```python
def test_with_context():
"""pytest provides detailed assertion introspection"""
result = calculate_total([1, 2, 3])
expected = 6
# pytest shows: assert 5 == 6
assert result == expected
```
### Custom Assertion Messages
```python
def test_with_message():
result = process_data(input_data)
assert result.is_valid, f"Expected valid result, got errors: {result.errors}"
```
### Approximate Comparisons
```python
import pytest
def test_float_comparison():
result = 0.1 + 0.2
assert result == pytest.approx(0.3)
# With tolerance
assert result == pytest.approx(0.3, abs=1e-9)
```
## Exception Testing
```python
import pytest
def test_raises_exception():
"""Test that function raises expected exception"""
with pytest.raises(ValueError):
validate_age(-1)
def test_exception_message():
"""Test exception message"""
with pytest.raises(ValueError, match="Age must be positive"):
validate_age(-1)
def test_exception_details():
"""Capture and inspect exception"""
with pytest.raises(ValidationError) as exc_info:
validate_user(name="", age=-1)
assert "name" in exc_info.value.errors
assert "age" in exc_info.value.errors
```
## Test Helpers
```python
# tests/helpers.py
def assert_user_equal(actual, expected):
"""Custom assertion helper"""
assert actual.id == expected.id
assert actual.name == expected.name
assert actual.email == expected.email
def create_test_user(**kwargs):
"""Test data factory"""
defaults = {
"name": "Test User",
"email": "test@example.com",
"age": 25,
}
defaults.update(kwargs)
return User(**defaults)
```
## Property-Based Testing
Using `hypothesis` for property-based testing:
```python
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
"""Test that addition is commutative"""
assert a + b == b + a
@given(st.lists(st.integers()))
def test_sort_idempotent(lst):
"""Test that sorting twice gives same result"""
sorted_once = sorted(lst)
sorted_twice = sorted(sorted_once)
assert sorted_once == sorted_twice
```
## Best Practices
1. **One assertion per test** (when possible)
2. **Use descriptive test names** - describe what's being tested
3. **Arrange-Act-Assert pattern** - clear test structure
4. **Use fixtures for setup** - avoid duplication
5. **Mock external dependencies** - keep tests fast and isolated
6. **Test edge cases** - empty inputs, None, boundaries
7. **Use parametrize** - test multiple scenarios efficiently
8. **Keep tests independent** - no shared state between tests
## Running Tests
```bash
# Run all tests
pytest
# Run specific file
pytest tests/test_user.py
# Run specific test
pytest tests/test_user.py::test_create_user
# Run with verbose output
pytest -v
# Run with output capture disabled
pytest -s
# Run in parallel (requires pytest-xdist)
pytest -n auto
# Run only failed tests from last run
pytest --lf
# Run failed tests first
pytest --ff
```
## When to Use This Skill
- Writing new Python tests
- Improving test coverage
- Setting up pytest infrastructure
- Debugging flaky tests
- Implementing integration tests
- Testing async Python code