| name | pytest-testing |
| description | Write pytest tests for Python code including unit tests, integration tests, fixtures, mocking, and parametrize. Use when writing tests for functions, classes, or complex business logic. |
Pytest Testing Specialist
Specialized in writing comprehensive pytest tests with fixtures, mocking, and parametrize.
When to Use This Skill
- Writing unit tests for functions and classes
- Creating integration tests
- Setting up test fixtures
- Mocking external dependencies
- Parametrizing tests for multiple scenarios
- Testing async code
Core Principles
- Test First (TDD): Write tests before implementation
- Independent Tests: Each test should run independently
- Clear Test Names: Test names describe expected behavior
- Fixture Reusability: Share setup code via fixtures
- Mock External Dependencies: Isolate unit tests from external systems
- One Assertion Focus: Prefer focused tests over complex multi-assertion tests
Implementation Guidelines
Basic Test Structure
# tests/test_user_service.py
import pytest
from app.services.user_service import UserService
from app.models.user import User
class TestUserService:
"""Test suite for UserService."""
def test_create_user_success(self):
"""Should create user with valid data."""
# Arrange
service = UserService()
user_data = {
"email": "john@example.com",
"name": "John Doe"
}
# Act
user = service.create_user(user_data)
# Assert
assert user.email == "john@example.com"
assert user.name == "John Doe"
assert user.id is not None
def test_create_user_invalid_email(self):
"""Should raise ValidationError for invalid email."""
# Arrange
service = UserService()
user_data = {
"email": "invalid",
"name": "John Doe"
}
# Act & Assert
with pytest.raises(ValueError, match="Invalid email"):
service.create_user(user_data)
def test_create_user_missing_name(self):
"""Should raise ValidationError when name is missing."""
service = UserService()
user_data = {"email": "john@example.com"}
with pytest.raises(ValueError, match="Name is required"):
service.create_user(user_data)
Using Fixtures
# tests/conftest.py
import pytest
from app.database import Database
from app.services.user_service import UserService
@pytest.fixture
def database():
"""Provide test database connection."""
db = Database(":memory:")
db.setup()
yield db
db.close()
@pytest.fixture
def user_service(database):
"""Provide UserService with test database."""
return UserService(database=database)
@pytest.fixture
def sample_user_data():
"""Provide sample user data for tests."""
return {
"email": "test@example.com",
"name": "Test User",
"age": 30
}
# tests/test_user_service.py
def test_create_user_with_fixtures(user_service, sample_user_data):
"""Test user creation using fixtures."""
user = user_service.create_user(sample_user_data)
assert user.email == sample_user_data["email"]
assert user.name == sample_user_data["name"]
Fixture Scopes
# conftest.py
import pytest
@pytest.fixture(scope="function") # Default: new instance per test
def function_scoped_db():
"""New database for each test."""
db = create_test_db()
yield db
db.cleanup()
@pytest.fixture(scope="class") # Shared within test class
def class_scoped_cache():
"""Cache shared across test class."""
cache = Cache()
yield cache
cache.clear()
@pytest.fixture(scope="module") # Once per module
def module_scoped_config():
"""Config loaded once per test module."""
return load_test_config()
@pytest.fixture(scope="session") # Once per test session
def session_scoped_app():
"""Application instance for entire test session."""
app = create_app()
yield app
app.shutdown()
Parametrize Tests
import pytest
@pytest.mark.parametrize("input_email,expected_valid", [
("user@example.com", True),
("admin@company.co.uk", True),
("invalid", False),
("@example.com", False),
("user@", False),
])
def test_email_validation(input_email, expected_valid):
"""Test email validation with various inputs."""
result = validate_email(input_email)
assert result == expected_valid
@pytest.mark.parametrize("age,expected_category", [
(5, "child"),
(15, "teenager"),
(25, "adult"),
(70, "senior"),
])
def test_age_category(age, expected_category):
"""Test age categorization."""
category = categorize_age(age)
assert category == expected_category
# Parametrize with IDs for better test output
@pytest.mark.parametrize("amount,discount,expected", [
(100, 0.1, 90),
(200, 0.2, 160),
(50, 0, 50),
], ids=["10% discount", "20% discount", "no discount"])
def test_calculate_discounted_price(amount, discount, expected):
"""Test price calculation with different discounts."""
result = calculate_price(amount, discount)
assert result == pytest.approx(expected)
Mocking External Dependencies
from unittest.mock import Mock, patch, MagicMock
import pytest
def test_send_email_success():
"""Test email sending with mocked SMTP client."""
# WHY: Mock SMTP to avoid actual email sending in tests
mock_smtp = Mock()
mock_smtp.send.return_value = True
service = NotificationService(smtp_client=mock_smtp)
result = service.send_welcome_email("user@example.com")
assert result is True
mock_smtp.send.assert_called_once_with(
to="user@example.com",
subject="Welcome",
body=pytest.mock.ANY # Match any body content
)
@patch('app.services.payment_service.PaymentGateway')
def test_process_payment(mock_payment_gateway):
"""Test payment processing with patched gateway."""
# Configure mock
mock_instance = mock_payment_gateway.return_value
mock_instance.charge.return_value = {
"status": "success",
"transaction_id": "txn_123"
}
# Test
service = PaymentService()
result = service.process_payment(100, "card_token")
# Verify
assert result["status"] == "success"
assert result["transaction_id"] == "txn_123"
mock_instance.charge.assert_called_once_with(100, "card_token")
def test_api_call_with_context_manager():
"""Test API call using patch as context manager."""
with patch('app.api.requests.get') as mock_get:
# WHY: Mock HTTP request to avoid external API dependency
mock_get.return_value.json.return_value = {"id": 1, "name": "Test"}
mock_get.return_value.status_code = 200
client = ApiClient()
result = client.fetch_user(1)
assert result["name"] == "Test"
mock_get.assert_called_once_with("https://api.example.com/users/1")
Testing Exceptions
import pytest
def test_divide_by_zero():
"""Should raise ZeroDivisionError."""
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_validation_error_message():
"""Should raise ValidationError with specific message."""
with pytest.raises(ValueError, match="Email is required"):
validate_user({"name": "John"})
def test_exception_details():
"""Should raise exception with correct details."""
with pytest.raises(NotFoundError) as exc_info:
get_user(999)
# Verify exception details
assert exc_info.value.resource == "User"
assert exc_info.value.id == 999
Testing Async Code
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async_fetch_user():
"""Test async user fetch."""
service = AsyncUserService()
user = await service.fetch_user(1)
assert user["id"] == 1
assert user["name"] is not None
@pytest.mark.asyncio
async def test_concurrent_operations():
"""Test concurrent async operations."""
service = AsyncOrderService()
# WHY: Test that concurrent operations work correctly
orders = await asyncio.gather(
service.fetch_order(1),
service.fetch_order(2),
service.fetch_order(3)
)
assert len(orders) == 3
assert all(order["id"] for order in orders)
@pytest.fixture
async def async_database():
"""Async fixture for database."""
db = await create_async_db()
yield db
await db.close()
@pytest.mark.asyncio
async def test_with_async_fixture(async_database):
"""Test using async fixture."""
result = await async_database.query("SELECT 1")
assert result is not None
Integration Tests
import pytest
@pytest.mark.integration
def test_user_registration_flow(database, email_service):
"""Test complete user registration flow.
WHY: Integration test verifying multiple components work together.
"""
# Arrange
registration_service = RegistrationService(
database=database,
email_service=email_service
)
user_data = {
"email": "newuser@example.com",
"name": "New User",
"password": "securepass123"
}
# Act
result = registration_service.register(user_data)
# Assert
assert result["success"] is True
assert result["user"]["id"] is not None
# Verify user exists in database
user = database.get_user_by_email("newuser@example.com")
assert user is not None
assert user.name == "New User"
# Verify welcome email was sent
email_service.send_welcome_email.assert_called_once_with(
"newuser@example.com"
)
@pytest.mark.integration
def test_order_processing_transaction(database):
"""Test order processing with database transaction."""
service = OrderService(database=database)
# Create order
order = service.create_order(
user_id=1,
items=[{"id": 1, "quantity": 2}],
total=200
)
# Verify order in database
stored_order = database.get_order(order.id)
assert stored_order.status == "pending"
assert stored_order.total == 200
Tools to Use
Write: Create new test filesEdit: Modify existing testsBash: Run pytest commands
Bash Commands
# Run all tests
pytest
# Run specific test file
pytest tests/test_user_service.py
# Run specific test function
pytest tests/test_user_service.py::test_create_user_success
# Run with verbose output
pytest -v
# Run with coverage
pytest --cov=app tests/
# Run only marked tests
pytest -m integration
pytest -m "not slow"
# Run with output
pytest -s # Show print statements
Workflow
- Understand Requirements: Clarify what needs to be tested
- Write Test First: Write failing test (Red)
- Verify Test Fails: Confirm test fails correctly
- Commit Test: Commit the failing test
- Implementation: Write code to pass test (Green)
- Run Tests: Verify tests pass
- Refactor: Improve code quality
- Run Coverage: Check test coverage
- Commit: Create atomic commit
Related Skills
python-core-development: For implementing code being testedpytest-api-testing: For testing API endpointspython-api-development: For FastAPI/Flask code being tested
Testing Fundamentals
Coding Standards
TDD Workflow
Follow Python TDD Workflow
Key Reminders
- Write tests BEFORE implementation (TDD)
- Use descriptive test names that explain expected behavior
- Keep tests independent and isolated
- Use fixtures for reusable test setup
- Mock external dependencies (APIs, databases, file I/O)
- Parametrize tests to cover multiple scenarios
- Test both success and error cases
- Use appropriate fixture scopes for performance
- Run coverage checks to ensure adequate testing
- Commit tests separately from implementation