- name
- python-testing
- description
- Python-specific testing practices with pytest, fixtures, mocking, async testing, coverage configuration, and uv execution rules. Activate when working with pytest files, conftest.py, test directories, pyproject.toml testing configuration, or Python test-related tasks.
# Python Testing Practices
Python-specific testing patterns and best practices using pytest, complementing general testing-workflow skill.
## CRITICAL: UV Execution Rules
**NEVER use `-m` flag with uv run:**
```bash
# ✅ CORRECT - UV pytest execution
uv run pytest
uv run pytest -v
uv run pytest tests/unit/ -v
uv run pytest --cov=app --cov-report=html
# ❌ WRONG - Never add -m flag
# ❌ uv run -m pytest
# ❌ uv run -m pytest -v
```
**Always use `uv run pytest` directly** (never call pytest directly in uv projects).
---
## Pytest Fixtures
### Fixture Scopes
Pytest fixtures support different scopes for setup/teardown control:
```python
@pytest.fixture(scope="session")
def database_connection():
"""Created once per test session - expensive setup."""
connection = setup_expensive_database()
yield connection
connection.cleanup()
@pytest.fixture(scope="module")
def api_client():
"""Created once per test module."""
return APIClient(config="test")
@pytest.fixture(scope="class")
def service_instance():
"""Created once per test class."""
return Service()
@pytest.fixture(scope="function") # Default
def user():
"""Created for each test function - isolated."""
return User(email="test@example.com")
```
### Fixture Setup and Teardown
Use `yield` pattern for setup/teardown with proper cleanup:
```python
@pytest.fixture
def database():
"""Fixture with setup and teardown."""
# Setup
db = Database(":memory:")
db.initialize()
db.create_schema()
# Provide to test
yield db
# Teardown
db.close()
@pytest.fixture
def mock_file(tmp_path):
"""Create temporary file that auto-cleans up."""
temp_file = tmp_path / "test.txt"
temp_file.write_text("test data")
yield temp_file
# Cleanup automatic with tmp_path
```
### Fixture Dependency Injection
Fixtures can depend on other fixtures:
```python
@pytest.fixture
def database():
"""Base database fixture."""
db = Database(":memory:")
db.initialize()
yield db
db.close()
@pytest.fixture
def user_service(database):
"""Service depending on database fixture."""
return UserService(database)
@pytest.fixture
def authenticated_user(user_service):
"""User depending on service fixture."""
user = user_service.create_user(
username="testuser",
email="test@example.com",
password="secure"
)
yield user
user_service.delete_user(user.id)
```
---
## conftest.py Patterns
### Central Fixture Location
Place shared fixtures in `conftest.py` at appropriate levels:
```
tests/
├── conftest.py # Session/module level fixtures
├── unit/
│ ├── conftest.py # Unit-specific fixtures
│ └── test_*.py
└── integration/
├── conftest.py # Integration-specific fixtures
└── test_*.py
```
### Example conftest.py
```python
# tests/conftest.py
import pytest
from app.services import UserService
from app.database import Database
@pytest.fixture(scope="session")
def database_connection():
"""Database connection for entire session."""
db = Database(":memory:")
db.initialize()
db.run_migrations()
yield db
db.close()
@pytest.fixture
def user_service(database_connection):
"""UserService with test database."""
return UserService(database_connection)
@pytest.fixture
def sample_user_data():
"""Standard user data for tests."""
return {
"username": "testuser",
"email": "test@example.com",
"password": "secure123",
"age": 25
}
@pytest.fixture
def temp_config_file(tmp_path):
"""Create temporary config file."""
config_file = tmp_path / "config.json"
config_file.write_text('{"setting": "value"}')
return config_file
```
---
## Parametrized Testing
### Basic Parametrization
Use `@pytest.mark.parametrize` for multiple test inputs:
```python
import pytest
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
("MiXeD", "MIXED"),
])
def test_uppercase(input, expected):
"""Test uppercase conversion with multiple inputs."""
assert input.upper() == expected
```
### Multiple Parameters
```python
@pytest.mark.parametrize("method,expected_status", [
("GET", 200),
("POST", 201),
("PUT", 200),
("DELETE", 204),
])
def test_http_methods(client, method, expected_status):
"""Test different HTTP methods."""
response = client.request(method, "/api/resource")
assert response.status_code == expected_status
```
### Parametrized Fixtures
```python
@pytest.mark.parametrize("user_role", ["admin", "user", "guest"])
def test_permission_levels(user_role):
"""Test different user permission levels."""
user = User(role=user_role)
assert user.can_access_dashboard() == (user_role in ["admin", "user"])
```
---
## Async Testing
### Marking Async Tests
Use `@pytest.mark.asyncio` decorator:
```python
import pytest
@pytest.mark.asyncio
async def test_async_fetch_data():
"""Test async data fetching."""
result = await fetch_data_async("user123")
assert result is not None
assert result["id"] == "user123"
@pytest.mark.asyncio
async def test_concurrent_requests():
"""Test multiple concurrent async operations."""
results = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3),
)
assert len(results) == 3
```
### Async Fixtures
```python
@pytest.fixture
async def async_client():
"""Async HTTP client fixture."""
client = AsyncHTTPClient()
await client.connect()
yield client
await client.disconnect()
@pytest.mark.asyncio
async def test_api_call(async_client):
"""Test with async fixture."""
response = await async_client.get("/api/users")
assert response.status_code == 200
```
---
## Mocking Patterns
### Mock Objects and Patching
```python
from unittest.mock import Mock, patch, MagicMock
import pytest
def test_send_email_success():
"""Test email sending with mocked SMTP."""
# Create mock object
mock_smtp = Mock()
with patch("smtplib.SMTP", return_value=mock_smtp):
result = send_email("test@example.com", "Subject", "Body")
# Verify mock was called correctly
mock_smtp.send_message.assert_called_once()
assert result is True
def test_api_call_with_mock():
"""Test API call with mocked response."""
with patch("requests.get") as mock_get:
mock_get.return_value.json.return_value = {"status": "ok"}
result = fetch_api_data()
mock_get.assert_called_once_with("https://api.example.com/data")
assert result["status"] == "ok"
```
### Fixture-Based Mocking
```python
@pytest.fixture
def mock_database():
"""Provide mocked database."""
with patch("app.database.Database") as mock_db:
mock_db.return_value.query.return_value = {"id": 1, "name": "Test"}
yield mock_db
def test_user_service_with_mock(mock_database):
"""Test service with mocked database dependency."""
service = UserService(mock_database.return_value)
user = service.get_user(1)
assert user["name"] == "Test"
@pytest.fixture
def mock_api_client():
"""Provide mocked API client."""
with patch("app.client.APIClient") as mock:
mock.return_value.get.return_value = {"status": "ok"}
mock.return_value.post.return_value = {"id": 123}
yield mock
```
### Spy on Real Objects
```python
from unittest.mock import MagicMock
def test_spy_on_method():
"""Spy on actual method calls."""
obj = RealObject()
obj.method = MagicMock(side_effect=obj.method)
result = obj.method("arg1")
obj.method.assert_called_once_with("arg1")
assert result == expected_value
```
---
## Exception Testing
### Testing with Context Managers
```python
import pytest
from app.exceptions import UserNotFoundError, ValidationError
def test_exception_raised():
"""Test that correct exception is raised."""
with pytest.raises(UserNotFoundError) as exc_info:
user_service.get_user("nonexistent_id")
assert "nonexistent_id" in str(exc_info.value)
def test_validation_error():
"""Test validation error with message check."""
user_data = {
"username": "testuser",
"email": "invalid-email", # Invalid format
"age": 25
}
with pytest.raises(ValidationError) as exc_info:
user_service.create_user(user_data)
assert "email" in str(exc_info.value)
def test_exception_type_matching():
"""Test matching specific exception type."""
with pytest.raises((ValueError, TypeError)):
process_data(None)
```
---
## pyproject.toml Configuration
### Complete Pytest Configuration
```toml
[tool.pytest.ini_options]
# Test discovery
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
# Output and behavior
addopts = [
"-v",
"--strict-markers",
"--tb=short",
"--cov=app",
"--cov-report=term-missing",
"--cov-report=html",
]
# Custom markers
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
"asyncio: marks tests as async tests",
]
# Filter warnings as errors
filterwarnings = [
"error",
"ignore::DeprecationWarning:setuptools", # Ignore specific warnings if needed
]
```
### Coverage Configuration
```toml
[tool.coverage.run]
source = ["app"]
omit = [
"*/tests/*",
"*/migrations/*",
"*/__init__.py",
"*/__main__.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
عرض على GitHub