add new
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 12s
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 12s
This commit is contained in:
434
.agents/skills/pytest/references/api-testing.md
Normal file
434
.agents/skills/pytest/references/api-testing.md
Normal 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
|
||||
```
|
||||
290
.agents/skills/pytest/references/async-testing.md
Normal file
290
.agents/skills/pytest/references/async-testing.md
Normal 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()
|
||||
```
|
||||
296
.agents/skills/pytest/references/fixtures.md
Normal file
296
.agents/skills/pytest/references/fixtures.md
Normal 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()
|
||||
```
|
||||
354
.agents/skills/pytest/references/mocking.md
Normal file
354
.agents/skills/pytest/references/mocking.md
Normal 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!",
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user