- name
- testing-pytest
- description
- Pytest testing skill with modern patterns - use for writing Python tests with fixtures, parametrization, and modular design
# Pytest Testing Skill
Use this skill when writing Python tests with pytest framework.
<IMPORTANT>
Follow pytest best practices for modern, modular, and reusable test design.
**Pytest is the standard.** Use pytest for all Python testing.
**Tests MUST be isolated.** Each test should be independent and not rely on execution order or state from other tests.
**Fixtures over setup/teardown.** Use pytest fixtures for test dependencies and resource management, not setUp/tearDown methods.
**Parametrize for data-driven tests.** Use `@pytest.mark.parametrize` instead of writing multiple similar test functions.
**Organize tests logically.** Mirror the source code structure in your test directory. Group related tests in classes when appropriate.
**Test behavior, not implementation.** Focus on testing what the code does, not how it does it. This makes tests resilient to refactoring.
</IMPORTANT>
## Pytest Basics
### Test Discovery and Naming
Pytest automatically discovers tests following these conventions:
```python
# File naming: test_*.py or *_test.py
test_user.py # ✓ Discovered
user_test.py # ✓ Discovered
test_models.py # ✓ Discovered
models.py # ✗ Not discovered
# Function naming: test_*
def test_user_creation(): # ✓ Discovered
pass
def test_invalid_email(): # ✓ Discovered
pass
def helper_function(): # ✗ Not a test (no test_ prefix)
pass
# Class naming: Test* (no __init__)
class TestUser: # ✓ Discovered
def test_creation(self):
pass
class TestUserValidation: # ✓ Discovered
def test_email(self):
pass
class UserHelper: # ✗ Not a test class
pass
```
### Basic Test Structure
```python
from myapp.models import User
def test_user_creation():
"""Test that a user can be created with valid data."""
# Arrange
username = "alice"
email = "alice@example.com"
# Act
user = User(username=username, email=email)
# Assert
assert user.username == "alice"
assert user.email == "alice@example.com"
assert user.is_active is True # Default value
def test_user_invalid_email():
"""Test that invalid email raises ValueError."""
with pytest.raises(ValueError, match="Invalid email"):
User(username="bob", email="not-an-email")
```
### Running Tests
```bash
# Run all tests
pytest
# Run specific file
pytest tests/test_user.py
# Run specific test
pytest tests/test_user.py::test_user_creation
# Run tests matching pattern
pytest -k "user and not invalid"
# Run with verbose output
pytest -v
# Run with coverage
pytest --cov=myapp --cov-report=html
# Run and stop at first failure
pytest -x
# Run last failed tests
pytest --lf
# Run in parallel (requires pytest-xdist)
pytest -n auto
```
## Assertions
### Basic Assertions
```python
import pytest
def test_assertions():
"""Demonstrate pytest assertion introspection."""
# Equality
assert result == expected
assert user.name == "Alice"
# Identity
assert value is None
assert obj is not None
# Membership
assert "admin" in user.roles
assert item not in processed_items
# Comparison
assert age >= 18
assert count < MAX_COUNT
# Boolean
assert user.is_active
assert not user.is_deleted
# Type checking
assert isinstance(result, dict)
assert isinstance(user, User)
def test_approximate_equality():
"""Test floating point with tolerance."""
import math
result = math.sqrt(2) ** 2
assert result == pytest.approx(2.0, rel=1e-9)
def test_assertion_messages():
"""Add custom messages to assertions."""
assert user.age >= 18, f"User {user.name} is underage: {user.age}"
```
### Exception Testing
```python
import pytest
def test_exception_raised():
"""Test that exception is raised."""
with pytest.raises(ValueError):
process_invalid_data()
def test_exception_with_message():
"""Test exception message matches pattern."""
with pytest.raises(ValueError, match="Invalid email format"):
User(email="not-an-email")
def test_exception_details():
"""Test exception attributes."""
with pytest.raises(APIError) as exc_info:
call_api_with_invalid_token()
assert exc_info.value.status_code == 401
assert "authentication" in str(exc_info.value).lower()
def test_no_exception():
"""Test that no exception is raised."""
# Just call the function - if it raises, test fails
result = process_valid_data()
assert result is not None
```
### Warnings Testing
```python
import pytest
import warnings
def test_deprecated_function():
"""Test that deprecated function shows warning."""
with pytest.warns(DeprecationWarning, match="deprecated"):
old_function()
def test_warning_details():
"""Test warning details."""
with pytest.warns(UserWarning) as warning_list:
trigger_warning()
assert len(warning_list) == 1
assert "careful" in str(warning_list[0].message)
```
## Fixtures
### Basic Fixtures
```python
import pytest
from myapp.database import Database
from myapp.models import User
@pytest.fixture
def database():
"""Provide a database connection for tests."""
db = Database(":memory:")
db.create_tables()
yield db # Provide to test
db.close() # Cleanup after test
@pytest.fixture
def sample_user():
"""Provide a sample user for tests."""
return User(username="alice", email="alice@example.com")
def test_user_save(database, sample_user):
"""Test saving user to database."""
# Fixtures are automatically injected by name
database.save(sample_user)
retrieved = database.get_user(sample_user.id)
assert retrieved.username == "alice"
```
### Fixture Scopes
```python
import pytest
# Function scope (default): Run once per test function
@pytest.fixture
def temp_file():
"""Create temp file for each test."""
file = create_temp_file()
yield file
file.delete()
# Class scope: Run once per test class
@pytest.fixture(scope="class")
def database_connection():
"""Create database connection for test class."""
conn = Database.connect()
yield conn
conn.close()
# Module scope: Run once per test module
@pytest.fixture(scope="module")
def api_client():
"""Create API client for entire module."""
client = APIClient()
client.authenticate()
yield client
client.logout()
# Session scope: Run once per test session
@pytest.fixture(scope="session")
def docker_container():
"""Start Docker container for entire test session."""
container = start_docker_postgres()
yield container
container.stop()
```
### Fixture Dependencies
```python
import pytest
@pytest.fixture
def database():
"""Provide database connection."""
db = Database(":memory:")
db.create_tables()
yield db
db.close()
@pytest.fixture
def user_repository(database):
"""Provide user repository (depends on database fixture)."""
return UserRepository(database)
@pytest.fixture
def sample_users(user_repository):
"""Create sample users (depends on user_repository)."""
users = [
User(username="alice", email="alice@example.com"),
User(username="bob", email="bob@example.com"),
]
for user in users:
user_repository.save(user)
return users
def test_find_user(user_repository, sample_users):
"""Test finding user by username."""
# Both fixtures are injected automatically
user = user_repository.find_by_username("alice")
assert user is not None
assert user.email == "alice@example.com"
```
### Autouse Fixtures
```python
import pytest
@pytest.fixture(autouse=True)
def reset_state():
"""Reset global state before each test (runs automatically)."""
clear_cache()
reset_counters()
yield
# Cleanup after test
@pytest.fixture(autouse=True, scope="module")
def setup_logging():
"""Configure logging for all tests in module."""
configure_test_logging()
```
### Factory Fixtures
```python
import pytest
@pytest.fixture
def user_factory(database):
"""Fixture that returns a factory function for creating users."""
def _create_user(username: str, **kwargs):
user = User(username=username, **kwargs)
database.save(user)
return user
return _create_user
def test_multiple_users(user_factory):
"""Test with multiple users created by factory."""
alice = user_factory("alice", email="alice@example.com")
bob = user_factory("bob", email="bob@example.com", admin=True)
assert alice.username == "alice"
assert bob.is_admin is True
```
## Parametrization
### Basic Parametrization
```python
import pytest
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(4, 16),
(5, 25),
])
def test_square(input, expected):
"""Test square function with multiple inputs."""
assert square(input) == expected
@pytest.mark.parametrize("email", [
"user@example.com",
"test+tag@domain.org",
"name.surname@company.co.uk",
])
def test_valid_emails(email):
"""Test that valid emails are accepted."""
assert is_valid_email(email) is True
@pytest.mark.parametrize("invalid_email", [
"not-an-email",
"@example.com",
"user@",
"user name@example.com",
])
def test_invalid_emails(invalid_email):
"""Test that invalid emails are rejected."""
assert is_valid_email(invalid_email) is False
```
### Multiple Parameters
```python
import pytest
@pytest.mark.parametrize("username,email,expected_valid", [
("alice", "alice@example.com", True),
("bob", "bob@example.com", True),
("", "test@example.com", False), # Empty username
("alice", "not-an-email", False), # Invalid email
("a" * 100, "test@example.com", False), # Username too long
])
def test_user_validation(username, email, expected_valid):
"""Test user validation with various inputs."""
user = User(username=username, email=email)
assert user.is_valid() == expected_valid
```
### Parametrize with IDs
```python
import pytest
@pytest.mark.parametrize("input,expected", [
pytest.param(2, 4, id="two"),
pytest.param(3, 9, id="three"),
pytest.param(10, 100, id="ten"),
])
def test_square_with_ids(input, expected):
"""Test square with readable test IDs."""
assert square(input) == expected
# Alternative: ids as list
@pytest.mark.parametrize("value,result", [
(0, "zero"),
(1, "one"),
(5, "many"),
], ids=["zero", "one", "many"])
def test_number_name(value, result):
assert get_name(value) == result
```
### Combining Parametrize
```python
import pytest
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [3, 4])
def test_add(x, y):
"""Test all combinations: (1,3), (1,4), (2,3), (2,4)."""
result = x + y
assert result > 0
```
### Parametrizing Fixtures
```python
import pytest
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
"""Parametrized fixture - runs tests with each database."""
db_type = request.param
db = Database.create(db_type)
Ver en GitHub