Initial commit
This commit is contained in:
447
skills/python-testing/pytest-configuration.md
Normal file
447
skills/python-testing/pytest-configuration.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Pytest Configuration Guide
|
||||
|
||||
Pytest is a powerful testing framework for Python that makes it easy to write simple and scalable tests.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
uv add --dev pytest
|
||||
uv add --dev pytest-cov # For coverage
|
||||
uv add --dev pytest-mock # For mocking
|
||||
uv add --dev pytest-asyncio # For async tests
|
||||
```
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
Add to `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py", "*_test.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = [
|
||||
"--strict-markers",
|
||||
"--strict-config",
|
||||
"--showlocals",
|
||||
"-ra",
|
||||
]
|
||||
markers = [
|
||||
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||
"integration: marks tests as integration tests",
|
||||
"unit: marks tests as unit tests",
|
||||
]
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── src/
|
||||
│ └── project_name/
|
||||
│ └── module.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py # Shared fixtures
|
||||
│ ├── test_module.py # Unit tests
|
||||
│ ├── integration/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── test_api.py # Integration tests
|
||||
│ └── fixtures/
|
||||
│ └── sample_data.json
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Basic Test Function
|
||||
|
||||
```python
|
||||
def test_simple_addition():
|
||||
"""Test that addition works correctly."""
|
||||
result = add(2, 3)
|
||||
assert result == 5
|
||||
```
|
||||
|
||||
### Test Class
|
||||
|
||||
```python
|
||||
class TestCalculator:
|
||||
"""Tests for Calculator class."""
|
||||
|
||||
def test_addition(self):
|
||||
calc = Calculator()
|
||||
assert calc.add(2, 3) == 5
|
||||
|
||||
def test_subtraction(self):
|
||||
calc = Calculator()
|
||||
assert calc.subtract(5, 3) == 2
|
||||
```
|
||||
|
||||
### Using Assertions
|
||||
|
||||
```python
|
||||
def test_assertions():
|
||||
# Equality
|
||||
assert result == expected
|
||||
|
||||
# Boolean
|
||||
assert is_valid()
|
||||
assert not is_invalid()
|
||||
|
||||
# Membership
|
||||
assert item in collection
|
||||
assert key in dictionary
|
||||
|
||||
# Type checking
|
||||
assert isinstance(obj, MyClass)
|
||||
|
||||
# Exceptions
|
||||
with pytest.raises(ValueError):
|
||||
raise_error()
|
||||
|
||||
# Approximate equality (floats)
|
||||
assert result == pytest.approx(expected, rel=1e-5)
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
### Basic Fixture
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user():
|
||||
"""Create a sample user for testing."""
|
||||
return User(name="Alice", age=30)
|
||||
|
||||
def test_user_greeting(sample_user):
|
||||
assert sample_user.greet() == "Hello, I'm Alice"
|
||||
```
|
||||
|
||||
### Fixture Scopes
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="function") # Default, new instance per test
|
||||
def function_scope():
|
||||
return setup()
|
||||
|
||||
@pytest.fixture(scope="class") # One instance per test class
|
||||
def class_scope():
|
||||
return setup()
|
||||
|
||||
@pytest.fixture(scope="module") # One instance per module
|
||||
def module_scope():
|
||||
return setup()
|
||||
|
||||
@pytest.fixture(scope="session") # One instance per test session
|
||||
def session_scope():
|
||||
return setup()
|
||||
```
|
||||
|
||||
### Fixture Cleanup
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def resource():
|
||||
# Setup
|
||||
res = acquire_resource()
|
||||
yield res
|
||||
# Teardown
|
||||
res.cleanup()
|
||||
|
||||
# Or using context manager
|
||||
@pytest.fixture
|
||||
def database():
|
||||
with create_database() as db:
|
||||
yield db
|
||||
# Automatic cleanup
|
||||
```
|
||||
|
||||
### Fixture Dependencies
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def database():
|
||||
return Database()
|
||||
|
||||
@pytest.fixture
|
||||
def user_repository(database):
|
||||
return UserRepository(database)
|
||||
|
||||
def test_find_user(user_repository):
|
||||
user = user_repository.find(1)
|
||||
assert user is not None
|
||||
```
|
||||
|
||||
## Parametrized Tests
|
||||
|
||||
### Basic Parametrization
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
(2, 4),
|
||||
(3, 9),
|
||||
(4, 16),
|
||||
(5, 25),
|
||||
])
|
||||
def test_square(input, expected):
|
||||
assert square(input) == expected
|
||||
```
|
||||
|
||||
### Multiple Parameters
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("a,b,expected", [
|
||||
(1, 1, 2),
|
||||
(2, 3, 5),
|
||||
(10, -5, 5),
|
||||
])
|
||||
def test_addition(a, b, expected):
|
||||
assert add(a, b) == expected
|
||||
```
|
||||
|
||||
### Parametrizing Fixtures
|
||||
|
||||
```python
|
||||
@pytest.fixture(params=[1, 2, 3])
|
||||
def number(request):
|
||||
return request.param
|
||||
|
||||
def test_with_different_numbers(number):
|
||||
assert number > 0
|
||||
```
|
||||
|
||||
## Test Markers
|
||||
|
||||
### Built-in Markers
|
||||
|
||||
```python
|
||||
@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_feature():
|
||||
pass
|
||||
|
||||
@pytest.mark.xfail(reason="Known bug")
|
||||
def test_buggy_feature():
|
||||
pass
|
||||
```
|
||||
|
||||
### Custom Markers
|
||||
|
||||
```python
|
||||
# Define in pyproject.toml
|
||||
# markers = ["slow: marks tests as slow"]
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_expensive_operation():
|
||||
# Long-running test
|
||||
pass
|
||||
|
||||
# Run with: pytest -m "not slow"
|
||||
```
|
||||
|
||||
## Exception Testing
|
||||
|
||||
```python
|
||||
def test_raises_value_error():
|
||||
with pytest.raises(ValueError):
|
||||
raise ValueError("Invalid value")
|
||||
|
||||
def test_raises_with_message():
|
||||
with pytest.raises(ValueError, match="Invalid"):
|
||||
raise ValueError("Invalid value")
|
||||
|
||||
def test_exception_info():
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
raise ValueError("Invalid value")
|
||||
assert "Invalid" in str(exc_info.value)
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
### Using pytest-mock
|
||||
|
||||
```python
|
||||
def test_with_mock(mocker):
|
||||
# Mock a function
|
||||
mock = mocker.patch('module.function')
|
||||
mock.return_value = 42
|
||||
|
||||
result = call_function()
|
||||
assert result == 42
|
||||
mock.assert_called_once()
|
||||
|
||||
def test_mock_method(mocker):
|
||||
# Mock a method
|
||||
mock = mocker.patch.object(MyClass, 'method')
|
||||
mock.return_value = "mocked"
|
||||
|
||||
obj = MyClass()
|
||||
assert obj.method() == "mocked"
|
||||
```
|
||||
|
||||
### Using unittest.mock
|
||||
|
||||
```python
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
def test_with_mock():
|
||||
with patch('module.function') as mock:
|
||||
mock.return_value = 42
|
||||
result = call_function()
|
||||
assert result == 42
|
||||
```
|
||||
|
||||
## Async Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_function():
|
||||
result = await async_operation()
|
||||
assert result == expected
|
||||
|
||||
@pytest.fixture
|
||||
async def async_client():
|
||||
client = AsyncClient()
|
||||
yield client
|
||||
await client.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_async_fixture(async_client):
|
||||
result = await async_client.get("/endpoint")
|
||||
assert result.status_code == 200
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
pytest
|
||||
|
||||
# Specific file
|
||||
pytest tests/test_module.py
|
||||
|
||||
# Specific test
|
||||
pytest tests/test_module.py::test_function
|
||||
|
||||
# Specific class
|
||||
pytest tests/test_module.py::TestClass
|
||||
|
||||
# Pattern matching
|
||||
pytest -k "test_user"
|
||||
|
||||
# Markers
|
||||
pytest -m slow # Only slow tests
|
||||
pytest -m "not slow" # Exclude slow tests
|
||||
|
||||
# Last failed
|
||||
pytest --lf
|
||||
|
||||
# Failed first
|
||||
pytest --ff
|
||||
|
||||
# Stop on first failure
|
||||
pytest -x
|
||||
|
||||
# Stop after N failures
|
||||
pytest --maxfail=3
|
||||
|
||||
# Verbose output
|
||||
pytest -v
|
||||
|
||||
# Show print statements
|
||||
pytest -s
|
||||
|
||||
# Show locals in tracebacks
|
||||
pytest --showlocals
|
||||
|
||||
# Parallel execution (requires pytest-xdist)
|
||||
pytest -n auto
|
||||
```
|
||||
|
||||
## conftest.py
|
||||
|
||||
Shared configuration and fixtures:
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def database():
|
||||
"""Create database for entire test session."""
|
||||
db = create_test_database()
|
||||
yield db
|
||||
db.drop()
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_database(database):
|
||||
"""Reset database before each test."""
|
||||
database.clear()
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Pytest configuration hook."""
|
||||
config.addinivalue_line(
|
||||
"markers", "integration: mark test as integration test"
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One assertion per test**: Keep tests focused
|
||||
2. **Descriptive names**: Use `test_user_creation_with_valid_data`
|
||||
3. **AAA pattern**: Arrange, Act, Assert
|
||||
4. **Use fixtures**: Avoid duplicating setup code
|
||||
5. **Test edge cases**: Not just happy paths
|
||||
6. **Fast tests**: Keep unit tests under 100ms
|
||||
7. **Independent tests**: Tests should not depend on each other
|
||||
8. **Clear assertions**: Make failures obvious
|
||||
|
||||
## Example Test File
|
||||
|
||||
```python
|
||||
"""Tests for user module."""
|
||||
import pytest
|
||||
from project_name.models import User
|
||||
|
||||
@pytest.fixture
|
||||
def valid_user_data():
|
||||
"""Valid user data for testing."""
|
||||
return {
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"age": 30
|
||||
}
|
||||
|
||||
class TestUser:
|
||||
"""Tests for User model."""
|
||||
|
||||
def test_create_user(self, valid_user_data):
|
||||
"""Test creating a user with valid data."""
|
||||
# Arrange & Act
|
||||
user = User(**valid_user_data)
|
||||
|
||||
# Assert
|
||||
assert user.name == "Alice"
|
||||
assert user.email == "alice@example.com"
|
||||
assert user.age == 30
|
||||
|
||||
def test_user_greeting(self, valid_user_data):
|
||||
"""Test user greeting message."""
|
||||
user = User(**valid_user_data)
|
||||
assert user.greet() == "Hello, I'm Alice"
|
||||
|
||||
@pytest.mark.parametrize("age", [-1, 0, 151])
|
||||
def test_invalid_age(self, valid_user_data, age):
|
||||
"""Test that invalid ages raise ValueError."""
|
||||
valid_user_data["age"] = age
|
||||
with pytest.raises(ValueError, match="Invalid age"):
|
||||
User(**valid_user_data)
|
||||
```
|
||||
Reference in New Issue
Block a user