mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Revert "Add Kiro IDE support (.kiro/) (#548)"
This reverts commit ce828c1c3c.
This commit is contained in:
@@ -1,497 +0,0 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user