add new
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 12s

This commit is contained in:
Nguyen Duc Thao
2026-01-26 23:07:28 +07:00
parent 018f267fab
commit 3861b027b2
16 changed files with 3164 additions and 60 deletions

View File

@@ -0,0 +1,187 @@
---
name: pytest
description: |
Python testing with pytest framework for unit, integration, and API tests.
Use when: (1) Writing test cases for Python code, (2) Setting up pytest fixtures,
(3) Testing async functions with pytest-asyncio, (4) Mocking dependencies,
(5) Parameterizing tests, (6) Testing FastAPI/Flask endpoints, (7) Setting up test coverage,
(8) Creating test factories with factory_boy, (9) Configuring CI/CD test pipelines.
---
# Pytest Testing Skill
Comprehensive testing patterns for Python applications using pytest.
## Quick Reference
| Feature | Reference File |
|---------|----------------|
| Fixtures, conftest, scopes | [references/fixtures.md](references/fixtures.md) |
| Async testing, pytest-asyncio | [references/async-testing.md](references/async-testing.md) |
| Mocking, patching, spies | [references/mocking.md](references/mocking.md) |
| FastAPI/Flask endpoint testing | [references/api-testing.md](references/api-testing.md) |
## Dependencies
```toml
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
"pytest-cov>=4.1.0",
"httpx>=0.28.0", # Async HTTP client for API tests
"factory-boy>=3.3.0", # Test factories
"faker>=33.0.0", # Fake data generation
]
```
## Configuration
### pyproject.toml
```toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
filterwarnings = ["ignore::DeprecationWarning"]
[tool.coverage.run]
source = ["app"]
omit = ["*/tests/*", "*/__init__.py"]
```
## Basic Test Structure
```python
import pytest
# Simple test function
def test_addition():
assert 1 + 1 == 2
# Test class (group related tests)
class TestCalculator:
def test_add(self):
assert add(2, 3) == 5
def test_subtract(self):
assert subtract(5, 3) == 2
# Expected exceptions
def test_division_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
# Parametrized tests
@pytest.mark.parametrize("input,expected", [
(1, 1),
(2, 4),
(3, 9),
])
def test_square(input, expected):
assert square(input) == expected
```
## Fixtures
```python
import pytest
@pytest.fixture
def sample_user():
return {"name": "John", "email": "john@example.com"}
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn # Test runs here
conn.close() # Cleanup after test
# Use fixture in test
def test_user_name(sample_user):
assert sample_user["name"] == "John"
```
## Async Testing
```python
import pytest
@pytest.mark.asyncio
async def test_async_function():
result = await async_operation()
assert result == "success"
@pytest.fixture
async def async_client():
async with AsyncClient() as client:
yield client
```
## Mocking
```python
from unittest.mock import patch, MagicMock, AsyncMock
def test_with_mock():
with patch("module.external_api") as mock_api:
mock_api.return_value = {"data": "mocked"}
result = function_using_api()
assert result["data"] == "mocked"
mock_api.assert_called_once()
# Async mock
@pytest.mark.asyncio
async def test_async_mock():
with patch("module.async_call", new_callable=AsyncMock) as mock:
mock.return_value = "result"
result = await function_with_async_call()
assert result == "result"
```
## Running Tests
```bash
# Run all tests
pytest
# Verbose output
pytest -v
# Run specific file
pytest tests/test_users.py
# Run specific test
pytest tests/test_users.py::test_create_user
# Run with coverage
pytest --cov=app --cov-report=term-missing
# Stop on first failure
pytest -x
# Run last failed tests
pytest --lf
# Run tests matching pattern
pytest -k "user and not delete"
```
## Project Structure
```
project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── services/
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── test_main.py
│ └── test_services/
└── pyproject.toml
```

View File

@@ -0,0 +1,434 @@
# API Testing
## Table of Contents
- [FastAPI Testing](#fastapi-testing)
- [Flask Testing](#flask-testing)
- [Test Factories](#test-factories)
- [Authentication Testing](#authentication-testing)
- [Database Testing](#database-testing)
## FastAPI Testing
### Setup
```python
# tests/conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlmodel import SQLModel
from app.main import app
from app.core.database import get_session
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
test_engine = create_async_engine(TEST_DATABASE_URL)
TestSession = async_sessionmaker(test_engine, class_=AsyncSession)
@pytest.fixture(scope="function")
async def db_session():
"""Fresh database for each test."""
async with test_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
async with TestSession() as session:
yield session
async with test_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.drop_all)
@pytest.fixture
async def client(db_session):
"""Test client with dependency override."""
async def override_session():
yield db_session
app.dependency_overrides[get_session] = override_session
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as ac:
yield ac
app.dependency_overrides.clear()
```
### Basic Endpoint Tests
```python
import pytest
from httpx import AsyncClient
class TestUsers:
@pytest.mark.asyncio
async def test_create_user(self, client: AsyncClient):
response = await client.post(
"/api/users",
json={"email": "test@example.com", "name": "Test User"},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert "id" in data
@pytest.mark.asyncio
async def test_get_user(self, client: AsyncClient, test_user):
response = await client.get(f"/api/users/{test_user.id}")
assert response.status_code == 200
assert response.json()["id"] == test_user.id
@pytest.mark.asyncio
async def test_get_user_not_found(self, client: AsyncClient):
response = await client.get("/api/users/99999")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_update_user(self, client: AsyncClient, test_user, auth_headers):
response = await client.patch(
f"/api/users/{test_user.id}",
json={"name": "Updated Name"},
headers=auth_headers,
)
assert response.status_code == 200
assert response.json()["name"] == "Updated Name"
@pytest.mark.asyncio
async def test_delete_user(self, client: AsyncClient, test_user, admin_headers):
response = await client.delete(
f"/api/users/{test_user.id}",
headers=admin_headers,
)
assert response.status_code == 204
```
### Testing Query Parameters
```python
@pytest.mark.asyncio
async def test_list_users_pagination(self, client: AsyncClient, auth_headers):
response = await client.get(
"/api/users",
params={"page": 1, "per_page": 10, "sort": "-created_at"},
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert "items" in data
assert "total" in data
assert data["page"] == 1
@pytest.mark.asyncio
async def test_search_users(self, client: AsyncClient, auth_headers):
response = await client.get(
"/api/users",
params={"q": "john", "active": True},
headers=auth_headers,
)
assert response.status_code == 200
```
### Testing File Uploads
```python
import io
@pytest.mark.asyncio
async def test_upload_file(self, client: AsyncClient, auth_headers):
file_content = b"Hello, World!"
files = {"file": ("test.txt", io.BytesIO(file_content), "text/plain")}
response = await client.post(
"/api/files/upload",
files=files,
headers=auth_headers,
)
assert response.status_code == 201
assert response.json()["filename"] == "test.txt"
```
## Flask Testing
### Setup
```python
import pytest
from app import create_app
from app.extensions import db
@pytest.fixture
def app():
"""Create application for testing."""
app = create_app("testing")
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Test client."""
return app.test_client()
@pytest.fixture
def runner(app):
"""CLI test runner."""
return app.test_cli_runner()
```
### Flask Test Examples
```python
def test_create_user(client):
response = client.post(
"/api/users",
json={"email": "test@example.com", "name": "Test"},
)
assert response.status_code == 201
def test_get_user(client, test_user):
response = client.get(f"/api/users/{test_user.id}")
assert response.status_code == 200
assert response.json["email"] == test_user.email
def test_protected_route(client, auth_token):
response = client.get(
"/api/protected",
headers={"Authorization": f"Bearer {auth_token}"},
)
assert response.status_code == 200
```
## Test Factories
### Using factory_boy
```python
# tests/factories.py
import factory
from faker import Faker
from app.models import User, Project
fake = Faker()
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.LazyAttribute(lambda _: fake.email())
name = factory.LazyAttribute(lambda _: fake.name())
hashed_password = "hashed_password"
is_active = True
class ProjectFactory(factory.Factory):
class Meta:
model = Project
name = factory.LazyAttribute(lambda _: fake.company())
description = factory.LazyAttribute(lambda _: fake.sentence())
owner_id = None
# Async helper
async def create_user(session, **kwargs) -> User:
user = UserFactory(**kwargs)
session.add(user)
await session.commit()
await session.refresh(user)
return user
async def create_users(session, count: int = 5, **kwargs) -> list[User]:
users = [UserFactory(**kwargs) for _ in range(count)]
session.add_all(users)
await session.commit()
return users
```
### Using Factories in Tests
```python
from tests.factories import create_user, create_users
@pytest.mark.asyncio
async def test_list_users(client, db_session, auth_headers):
# Create 20 users
await create_users(db_session, count=20)
response = await client.get("/api/users", headers=auth_headers)
assert response.status_code == 200
assert response.json()["total"] >= 20
@pytest.mark.asyncio
async def test_filter_active_users(client, db_session, auth_headers):
await create_user(db_session, is_active=True)
await create_user(db_session, is_active=False)
response = await client.get(
"/api/users",
params={"active": True},
headers=auth_headers,
)
data = response.json()
assert all(u["is_active"] for u in data["items"])
```
## Authentication Testing
### Auth Fixtures
```python
from app.core.security import create_access_token, hash_password
@pytest.fixture
async def test_user(db_session):
"""Create a regular test user."""
user = User(
email="test@example.com",
hashed_password=hash_password("password123"),
is_active=True,
)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
return user
@pytest.fixture
async def admin_user(db_session):
"""Create an admin user."""
user = User(
email="admin@example.com",
hashed_password=hash_password("admin123"),
is_active=True,
is_admin=True,
)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
return user
@pytest.fixture
def auth_headers(test_user):
"""Headers with valid auth token."""
token = create_access_token({"sub": str(test_user.id)})
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def admin_headers(admin_user):
"""Headers with admin auth token."""
token = create_access_token({"sub": str(admin_user.id)})
return {"Authorization": f"Bearer {token}"}
```
### Auth Test Examples
```python
class TestAuth:
@pytest.mark.asyncio
async def test_login_success(self, client, test_user):
response = await client.post(
"/api/auth/login",
data={"username": test_user.email, "password": "password123"},
)
assert response.status_code == 200
assert "access_token" in response.json()
@pytest.mark.asyncio
async def test_login_wrong_password(self, client, test_user):
response = await client.post(
"/api/auth/login",
data={"username": test_user.email, "password": "wrong"},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_protected_endpoint_no_auth(self, client):
response = await client.get("/api/protected")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_admin_endpoint_regular_user(self, client, auth_headers):
response = await client.delete(
"/api/admin/users/1",
headers=auth_headers,
)
assert response.status_code == 403
```
## Database Testing
### Transaction Rollback Pattern
```python
@pytest.fixture
async def db_session(engine):
"""Each test runs in a transaction that rolls back."""
async with engine.connect() as conn:
await conn.begin()
async with AsyncSession(bind=conn) as session:
yield session
await conn.rollback()
```
### Testing Repository Layer
```python
from app.repositories import UserRepository
class TestUserRepository:
@pytest.mark.asyncio
async def test_create(self, db_session):
repo = UserRepository(db_session)
user = await repo.create({
"email": "test@example.com",
"hashed_password": "hash",
})
assert user.id is not None
assert user.email == "test@example.com"
@pytest.mark.asyncio
async def test_get_by_email(self, db_session, test_user):
repo = UserRepository(db_session)
user = await repo.get_by_email(test_user.email)
assert user.id == test_user.id
@pytest.mark.asyncio
async def test_soft_delete(self, db_session, test_user):
repo = UserRepository(db_session)
await repo.soft_delete(test_user.id)
user = await repo.get_by_id(test_user.id)
assert user.status == 0 # Soft deleted
```

View File

@@ -0,0 +1,290 @@
# Async Testing
## Table of Contents
- [Setup](#setup)
- [Basic Async Tests](#basic-async-tests)
- [Async Fixtures](#async-fixtures)
- [Testing Async Generators](#testing-async-generators)
- [Timeouts](#timeouts)
## Setup
### Installation
```bash
pip install pytest-asyncio
```
### Configuration
```toml
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto" # Automatically handle async tests
# OR
asyncio_mode = "strict" # Require explicit @pytest.mark.asyncio
```
### With pytest.ini
```ini
[pytest]
asyncio_mode = auto
```
## Basic Async Tests
### Simple Async Test
```python
import pytest
# With asyncio_mode = "auto", decorator is optional
async def test_async_function():
result = await async_operation()
assert result == "success"
# With asyncio_mode = "strict", decorator is required
@pytest.mark.asyncio
async def test_async_with_marker():
result = await async_operation()
assert result == "success"
```
### Async Test Class
```python
@pytest.mark.asyncio
class TestAsyncOperations:
async def test_fetch_data(self):
data = await fetch_data()
assert data is not None
async def test_process_data(self):
result = await process_data({"key": "value"})
assert result["processed"] is True
```
### Testing Async Exceptions
```python
import pytest
@pytest.mark.asyncio
async def test_async_exception():
with pytest.raises(ValueError):
await async_function_that_raises()
@pytest.mark.asyncio
async def test_async_exception_message():
with pytest.raises(ValueError, match="Invalid input"):
await validate_input("")
```
## Async Fixtures
### Basic Async Fixture
```python
import pytest
@pytest.fixture
async def async_client():
"""Async fixture for HTTP client."""
async with httpx.AsyncClient() as client:
yield client
@pytest.mark.asyncio
async def test_api_call(async_client):
response = await async_client.get("https://api.example.com/data")
assert response.status_code == 200
```
### Async Database Fixture
```python
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
@pytest.fixture(scope="session")
def event_loop():
"""Create event loop for session-scoped async fixtures."""
import asyncio
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def engine():
"""Session-scoped async engine."""
engine = create_async_engine("postgresql+asyncpg://...")
yield engine
await engine.dispose()
@pytest.fixture
async def db_session(engine):
"""Function-scoped database session with rollback."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with AsyncSession(engine) as session:
yield session
await session.rollback()
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
```
### Fixture with Async Setup/Teardown
```python
@pytest.fixture
async def resource():
# Async setup
resource = await create_resource()
await resource.initialize()
yield resource
# Async teardown
await resource.cleanup()
await resource.close()
```
## Testing Async Generators
### Async Generator Function
```python
async def async_data_generator():
for i in range(5):
await asyncio.sleep(0.1)
yield i
@pytest.mark.asyncio
async def test_async_generator():
results = []
async for item in async_data_generator():
results.append(item)
assert results == [0, 1, 2, 3, 4]
```
### Async Context Manager
```python
from contextlib import asynccontextmanager
@asynccontextmanager
async def async_resource():
resource = await create_resource()
try:
yield resource
finally:
await resource.close()
@pytest.mark.asyncio
async def test_async_context_manager():
async with async_resource() as resource:
result = await resource.operation()
assert result is not None
```
## Timeouts
### Test Timeout
```python
import asyncio
import pytest
@pytest.mark.asyncio
@pytest.mark.timeout(5) # 5 second timeout
async def test_slow_operation():
result = await potentially_slow_operation()
assert result is not None
```
### Using asyncio.wait_for
```python
@pytest.mark.asyncio
async def test_with_timeout():
try:
result = await asyncio.wait_for(
slow_operation(),
timeout=2.0
)
assert result is not None
except asyncio.TimeoutError:
pytest.fail("Operation timed out")
```
### Testing Timeout Behavior
```python
@pytest.mark.asyncio
async def test_timeout_is_raised():
"""Verify function properly times out."""
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(
never_completes(),
timeout=0.1
)
```
## Concurrent Tests
### Testing Concurrent Operations
```python
@pytest.mark.asyncio
async def test_concurrent_requests():
async with httpx.AsyncClient() as client:
# Run 10 requests concurrently
tasks = [
client.get(f"https://api.example.com/items/{i}")
for i in range(10)
]
responses = await asyncio.gather(*tasks)
assert all(r.status_code == 200 for r in responses)
```
### Testing Race Conditions
```python
@pytest.mark.asyncio
async def test_counter_thread_safety():
counter = AsyncCounter()
async def increment():
for _ in range(100):
await counter.increment()
# Run 10 concurrent incrementers
await asyncio.gather(*[increment() for _ in range(10)])
assert counter.value == 1000
```
## Event Loop Configuration
### Custom Event Loop
```python
import pytest
import asyncio
@pytest.fixture(scope="session")
def event_loop_policy():
"""Use uvloop for faster async tests."""
import uvloop
return uvloop.EventLoopPolicy()
@pytest.fixture(scope="session")
def event_loop(event_loop_policy):
asyncio.set_event_loop_policy(event_loop_policy)
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
```

View File

@@ -0,0 +1,296 @@
# Fixtures
## Table of Contents
- [Basic Fixtures](#basic-fixtures)
- [Fixture Scopes](#fixture-scopes)
- [Fixture Parameters](#fixture-parameters)
- [Conftest.py](#conftestpy)
- [Built-in Fixtures](#built-in-fixtures)
## Basic Fixtures
### Simple Fixture
```python
import pytest
@pytest.fixture
def user_data():
"""Return sample user data."""
return {
"name": "John Doe",
"email": "john@example.com",
"age": 30,
}
def test_user_name(user_data):
assert user_data["name"] == "John Doe"
def test_user_email(user_data):
assert "@" in user_data["email"]
```
### Fixture with Setup and Teardown
```python
@pytest.fixture
def database():
"""Setup database, yield, then cleanup."""
# Setup
db = create_database()
db.connect()
yield db # Test runs here
# Teardown
db.clear()
db.disconnect()
def test_insert(database):
database.insert({"id": 1, "name": "Test"})
assert database.count() == 1
```
### Fixture Returning Factory
```python
@pytest.fixture
def make_user():
"""Return a factory function for creating users."""
created_users = []
def _make_user(name: str, email: str = None):
user = User(name=name, email=email or f"{name}@example.com")
created_users.append(user)
return user
yield _make_user
# Cleanup all created users
for user in created_users:
user.delete()
def test_multiple_users(make_user):
user1 = make_user("Alice")
user2 = make_user("Bob")
assert user1.name != user2.name
```
## Fixture Scopes
```python
# Function scope (default) - runs for each test
@pytest.fixture(scope="function")
def fresh_data():
return {"count": 0}
# Class scope - runs once per test class
@pytest.fixture(scope="class")
def class_resource():
return ExpensiveResource()
# Module scope - runs once per test module
@pytest.fixture(scope="module")
def module_connection():
conn = create_connection()
yield conn
conn.close()
# Session scope - runs once per test session
@pytest.fixture(scope="session")
def session_config():
return load_config()
```
### Scope Hierarchy
```
session (once per test run)
└── package (once per test package)
└── module (once per test file)
└── class (once per test class)
└── function (once per test function)
```
## Fixture Parameters
### Parametrized Fixtures
```python
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database_type(request):
"""Run tests with each database type."""
return request.param
def test_connection(database_type):
# This test runs 3 times, once for each database
db = create_database(database_type)
assert db.connect()
```
### Fixture with IDs
```python
@pytest.fixture(params=[
pytest.param({"admin": True}, id="admin"),
pytest.param({"admin": False}, id="regular"),
])
def user_config(request):
return request.param
# Test output shows: test_permissions[admin], test_permissions[regular]
```
### Indirect Parametrization
```python
@pytest.fixture
def user(request):
"""Create user based on parameter."""
role = request.param
return User(role=role)
@pytest.mark.parametrize("user", ["admin", "editor", "viewer"], indirect=True)
def test_user_access(user):
# user fixture receives each role as request.param
assert user.role in ["admin", "editor", "viewer"]
```
## Conftest.py
### tests/conftest.py
```python
"""Shared fixtures for all tests."""
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Session-scoped database engine
@pytest.fixture(scope="session")
def engine():
return create_engine("sqlite:///:memory:")
# Function-scoped session with transaction rollback
@pytest.fixture(scope="function")
def db_session(engine):
connection = engine.connect()
transaction = connection.begin()
Session = sessionmaker(bind=connection)
session = Session()
yield session
session.close()
transaction.rollback()
connection.close()
# Shared test data
@pytest.fixture
def sample_products():
return [
{"name": "Widget", "price": 9.99},
{"name": "Gadget", "price": 19.99},
{"name": "Gizmo", "price": 29.99},
]
```
### Nested Conftest Files
```
tests/
├── conftest.py # Available to all tests
├── unit/
│ ├── conftest.py # Available to unit tests only
│ └── test_models.py
└── integration/
├── conftest.py # Available to integration tests only
└── test_api.py
```
## Built-in Fixtures
### tmp_path / tmp_path_factory
```python
def test_create_file(tmp_path):
"""tmp_path provides unique temporary directory."""
file = tmp_path / "test.txt"
file.write_text("Hello, World!")
assert file.read_text() == "Hello, World!"
@pytest.fixture(scope="session")
def session_temp_dir(tmp_path_factory):
"""Create temp dir for entire session."""
return tmp_path_factory.mktemp("session_data")
```
### capsys / capfd
```python
def test_print_output(capsys):
"""Capture stdout/stderr."""
print("Hello")
captured = capsys.readouterr()
assert captured.out == "Hello\n"
def test_file_descriptor_output(capfd):
"""Capture at file descriptor level."""
import os
os.system("echo 'Hello'")
captured = capfd.readouterr()
assert "Hello" in captured.out
```
### monkeypatch
```python
def test_env_variable(monkeypatch):
"""Modify environment temporarily."""
monkeypatch.setenv("API_KEY", "test-key")
assert os.environ["API_KEY"] == "test-key"
def test_module_attribute(monkeypatch):
"""Modify module attribute temporarily."""
monkeypatch.setattr("module.CONFIG", {"debug": True})
assert module.CONFIG["debug"] is True
def test_dict_item(monkeypatch):
"""Modify dictionary temporarily."""
monkeypatch.setitem(app.settings, "DEBUG", True)
```
### request
```python
@pytest.fixture
def resource(request):
"""Access test context."""
print(f"Running: {request.node.name}")
print(f"Module: {request.module.__name__}")
print(f"Function: {request.function.__name__}")
# Access fixture parameters
if hasattr(request, "param"):
return create_resource(request.param)
return create_default_resource()
```
### Autouse Fixtures
```python
@pytest.fixture(autouse=True)
def setup_logging():
"""Automatically runs for every test."""
logging.basicConfig(level=logging.DEBUG)
yield
logging.shutdown()
@pytest.fixture(autouse=True, scope="session")
def global_setup():
"""Run once at session start."""
initialize_system()
yield
cleanup_system()
```

View File

@@ -0,0 +1,354 @@
# Mocking
## Table of Contents
- [Basic Mocking](#basic-mocking)
- [Patching](#patching)
- [Mock Objects](#mock-objects)
- [Async Mocking](#async-mocking)
- [Fixture-Based Mocking](#fixture-based-mocking)
- [Common Patterns](#common-patterns)
## Basic Mocking
### MagicMock Basics
```python
from unittest.mock import MagicMock
def test_with_mock():
# Create a mock object
mock_service = MagicMock()
# Configure return value
mock_service.get_data.return_value = {"id": 1, "name": "Test"}
# Use mock
result = mock_service.get_data()
assert result["name"] == "Test"
# Verify call
mock_service.get_data.assert_called_once()
```
### Return Value Configuration
```python
from unittest.mock import MagicMock
mock = MagicMock()
# Simple return value
mock.method.return_value = 42
# Different returns on consecutive calls
mock.method.side_effect = [1, 2, 3]
assert mock.method() == 1
assert mock.method() == 2
assert mock.method() == 3
# Raise exception
mock.method.side_effect = ValueError("Error!")
# Dynamic return value
mock.method.side_effect = lambda x: x * 2
assert mock.method(5) == 10
```
## Patching
### patch Decorator
```python
from unittest.mock import patch
# Patch a function
@patch("mymodule.external_api")
def test_with_patched_api(mock_api):
mock_api.return_value = {"status": "ok"}
result = mymodule.call_api()
assert result["status"] == "ok"
# Patch multiple
@patch("mymodule.function_a")
@patch("mymodule.function_b")
def test_multiple_patches(mock_b, mock_a):
# Note: decorators apply bottom-up
mock_a.return_value = "a"
mock_b.return_value = "b"
```
### patch Context Manager
```python
from unittest.mock import patch
def test_with_context_manager():
with patch("mymodule.external_api") as mock_api:
mock_api.return_value = {"data": "mocked"}
result = mymodule.process()
assert result == {"data": "mocked"}
# Original function restored after with block
```
### patch.object
```python
from unittest.mock import patch
class MyService:
def fetch_data(self):
return "real data"
def test_patch_instance_method():
service = MyService()
with patch.object(service, "fetch_data", return_value="mocked"):
assert service.fetch_data() == "mocked"
```
### Patching Where Used
```python
# mymodule.py
from requests import get # 'get' is imported here
def fetch_url(url):
return get(url).text
# test_mymodule.py
# Patch where it's USED, not where it's DEFINED
@patch("mymodule.get") # NOT "requests.get"
def test_fetch_url(mock_get):
mock_get.return_value.text = "mocked response"
result = fetch_url("http://example.com")
assert result == "mocked response"
```
## Mock Objects
### Spec and Autospec
```python
from unittest.mock import MagicMock, create_autospec
class UserService:
def get_user(self, user_id: int) -> dict:
pass
def create_user(self, data: dict) -> dict:
pass
# Basic mock (allows any attribute)
mock = MagicMock()
mock.nonexistent_method() # Works, but shouldn't
# Spec mock (restricts to real attributes)
mock = MagicMock(spec=UserService)
# mock.nonexistent_method() # Raises AttributeError
# Autospec (also checks signatures)
mock = create_autospec(UserService)
# mock.get_user() # Raises TypeError (missing user_id)
mock.get_user(123) # Works
```
### Assertion Methods
```python
from unittest.mock import MagicMock, call
mock = MagicMock()
mock.method(1, 2, key="value")
mock.method(3, 4)
# Verify calls
mock.method.assert_called()
mock.method.assert_called_once() # Fails - called twice
mock.method.assert_called_with(3, 4)
mock.method.assert_any_call(1, 2, key="value")
# Check call count
assert mock.method.call_count == 2
# Check all calls
assert mock.method.call_args_list == [
call(1, 2, key="value"),
call(3, 4),
]
# Reset mock
mock.reset_mock()
assert mock.method.call_count == 0
```
## Async Mocking
### AsyncMock
```python
from unittest.mock import AsyncMock, patch
import pytest
@pytest.mark.asyncio
async def test_async_mock():
mock = AsyncMock(return_value={"data": "mocked"})
result = await mock()
assert result["data"] == "mocked"
@pytest.mark.asyncio
@patch("mymodule.async_fetch", new_callable=AsyncMock)
async def test_patched_async(mock_fetch):
mock_fetch.return_value = {"status": "ok"}
result = await mymodule.process()
assert result["status"] == "ok"
```
### Async Side Effects
```python
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_async_side_effect():
mock = AsyncMock()
# Return different values
mock.side_effect = [1, 2, 3]
assert await mock() == 1
assert await mock() == 2
# Async function as side effect
async def async_side_effect(x):
return x * 2
mock.side_effect = async_side_effect
assert await mock(5) == 10
```
## Fixture-Based Mocking
### Mock Fixtures
```python
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def mock_database():
"""Fixture providing mock database."""
mock_db = MagicMock()
mock_db.query.return_value = [{"id": 1}, {"id": 2}]
return mock_db
def test_with_mock_fixture(mock_database):
result = mock_database.query("SELECT * FROM users")
assert len(result) == 2
@pytest.fixture
def mock_external_api():
"""Fixture with patching."""
with patch("mymodule.external_api") as mock:
mock.return_value = {"status": "ok"}
yield mock
```
### Monkeypatch Fixture
```python
def test_with_monkeypatch(monkeypatch):
# Patch function
monkeypatch.setattr("mymodule.get_config", lambda: {"debug": True})
# Patch environment variable
monkeypatch.setenv("API_KEY", "test-key")
# Patch dictionary item
monkeypatch.setitem(mymodule.settings, "DEBUG", True)
# Patch attribute
monkeypatch.setattr(mymodule.client, "timeout", 5)
```
## Common Patterns
### Mock HTTP Responses
```python
from unittest.mock import patch, MagicMock
@patch("requests.get")
def test_http_request(mock_get):
# Configure mock response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": "test"}
mock_get.return_value = mock_response
result = fetch_data("http://api.example.com")
assert result == {"data": "test"}
mock_get.assert_called_once_with("http://api.example.com")
```
### Mock File Operations
```python
from unittest.mock import mock_open, patch
def test_read_file():
mock_file_content = "Hello, World!"
with patch("builtins.open", mock_open(read_data=mock_file_content)):
result = read_file("test.txt")
assert result == "Hello, World!"
def test_write_file():
m = mock_open()
with patch("builtins.open", m):
write_file("test.txt", "content")
m().write.assert_called_once_with("content")
```
### Mock Datetime
```python
from unittest.mock import patch
from datetime import datetime
@patch("mymodule.datetime")
def test_with_frozen_time(mock_datetime):
mock_datetime.now.return_value = datetime(2024, 1, 15, 12, 0, 0)
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
result = mymodule.get_timestamp()
assert result == "2024-01-15 12:00:00"
```
### Mock Class Instance
```python
from unittest.mock import patch, MagicMock
class EmailService:
def send(self, to, subject, body):
# Real implementation
pass
@patch("mymodule.EmailService")
def test_email_sent(MockEmailService):
# Configure mock instance
mock_instance = MagicMock()
MockEmailService.return_value = mock_instance
# Call function that uses EmailService
send_notification("user@example.com", "Hello!")
# Verify email was sent
mock_instance.send.assert_called_once_with(
"user@example.com",
"Notification",
"Hello!",
)
```