810 lines
24 KiB
Markdown
810 lines
24 KiB
Markdown
# Testing Guide
|
|
|
|
This guide covers comprehensive testing strategies for the FastAPI boilerplate, including unit tests, integration tests, and API testing.
|
|
|
|
## Test Setup
|
|
|
|
### Testing Dependencies
|
|
|
|
The boilerplate uses these testing libraries:
|
|
|
|
- **pytest** - Testing framework
|
|
- **pytest-asyncio** - Async test support
|
|
- **httpx** - Async HTTP client for API tests
|
|
- **pytest-cov** - Coverage reporting
|
|
- **faker** - Test data generation
|
|
|
|
### Test Configuration
|
|
|
|
#### pytest.ini
|
|
|
|
```ini
|
|
[tool:pytest]
|
|
testpaths = tests
|
|
python_files = test_*.py
|
|
python_classes = Test*
|
|
python_functions = test_*
|
|
addopts =
|
|
-v
|
|
--strict-markers
|
|
--strict-config
|
|
--cov=src
|
|
--cov-report=term-missing
|
|
--cov-report=html
|
|
--cov-report=xml
|
|
--cov-fail-under=80
|
|
markers =
|
|
unit: Unit tests
|
|
integration: Integration tests
|
|
api: API tests
|
|
slow: Slow tests
|
|
asyncio_mode = auto
|
|
```
|
|
|
|
#### Test Database Setup
|
|
|
|
Create `tests/conftest.py`:
|
|
|
|
```python
|
|
import asyncio
|
|
import pytest
|
|
import pytest_asyncio
|
|
from typing import AsyncGenerator
|
|
from httpx import AsyncClient
|
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from faker import Faker
|
|
|
|
from src.app.core.config import settings
|
|
from src.app.core.db.database import Base, async_get_db
|
|
from src.app.main import app
|
|
from src.app.models.user import User
|
|
from src.app.models.post import Post
|
|
from src.app.core.security import get_password_hash
|
|
|
|
# Test database configuration
|
|
TEST_DATABASE_URL = "postgresql+asyncpg://test_user:test_pass@localhost:5432/test_db"
|
|
|
|
# Create test engine and session
|
|
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
|
TestSessionLocal = sessionmaker(
|
|
test_engine, class_=AsyncSession, expire_on_commit=False
|
|
)
|
|
|
|
fake = Faker()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def async_session() -> AsyncGenerator[AsyncSession, None]:
|
|
"""Create a fresh database session for each test."""
|
|
async with test_engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
|
|
async with TestSessionLocal() as session:
|
|
yield session
|
|
|
|
async with test_engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.drop_all)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def async_client(async_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
|
"""Create an async HTTP client for testing."""
|
|
def get_test_db():
|
|
return async_session
|
|
|
|
app.dependency_overrides[async_get_db] = get_test_db
|
|
|
|
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
yield client
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def test_user(async_session: AsyncSession) -> User:
|
|
"""Create a test user."""
|
|
user = User(
|
|
name=fake.name(),
|
|
username=fake.user_name(),
|
|
email=fake.email(),
|
|
hashed_password=get_password_hash("testpassword123"),
|
|
is_superuser=False
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
await async_session.refresh(user)
|
|
return user
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def test_superuser(async_session: AsyncSession) -> User:
|
|
"""Create a test superuser."""
|
|
user = User(
|
|
name="Super Admin",
|
|
username="superadmin",
|
|
email="admin@test.com",
|
|
hashed_password=get_password_hash("superpassword123"),
|
|
is_superuser=True
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
await async_session.refresh(user)
|
|
return user
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def test_post(async_session: AsyncSession, test_user: User) -> Post:
|
|
"""Create a test post."""
|
|
post = Post(
|
|
title=fake.sentence(),
|
|
content=fake.text(),
|
|
created_by_user_id=test_user.id
|
|
)
|
|
async_session.add(post)
|
|
await async_session.commit()
|
|
await async_session.refresh(post)
|
|
return post
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def auth_headers(async_client: AsyncClient, test_user: User) -> dict:
|
|
"""Get authentication headers for a test user."""
|
|
login_data = {
|
|
"username": test_user.username,
|
|
"password": "testpassword123"
|
|
}
|
|
|
|
response = await async_client.post("/api/v1/auth/login", data=login_data)
|
|
token = response.json()["access_token"]
|
|
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def superuser_headers(async_client: AsyncClient, test_superuser: User) -> dict:
|
|
"""Get authentication headers for a test superuser."""
|
|
login_data = {
|
|
"username": test_superuser.username,
|
|
"password": "superpassword123"
|
|
}
|
|
|
|
response = await async_client.post("/api/v1/auth/login", data=login_data)
|
|
token = response.json()["access_token"]
|
|
|
|
return {"Authorization": f"Bearer {token}"}
|
|
```
|
|
|
|
## Unit Tests
|
|
|
|
### Model Tests
|
|
|
|
```python
|
|
# tests/test_models.py
|
|
import pytest
|
|
from datetime import datetime
|
|
from src.app.models.user import User
|
|
from src.app.models.post import Post
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUserModel:
|
|
"""Test User model functionality."""
|
|
|
|
async def test_user_creation(self, async_session):
|
|
"""Test creating a user."""
|
|
user = User(
|
|
name="Test User",
|
|
username="testuser",
|
|
email="test@example.com",
|
|
hashed_password="hashed_password"
|
|
)
|
|
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
await async_session.refresh(user)
|
|
|
|
assert user.id is not None
|
|
assert user.name == "Test User"
|
|
assert user.username == "testuser"
|
|
assert user.email == "test@example.com"
|
|
assert user.created_at is not None
|
|
assert user.is_superuser is False
|
|
assert user.is_deleted is False
|
|
|
|
async def test_user_relationships(self, async_session, test_user):
|
|
"""Test user relationships."""
|
|
post = Post(
|
|
title="Test Post",
|
|
content="Test content",
|
|
created_by_user_id=test_user.id
|
|
)
|
|
|
|
async_session.add(post)
|
|
await async_session.commit()
|
|
|
|
# Test relationship
|
|
await async_session.refresh(test_user)
|
|
assert len(test_user.posts) == 1
|
|
assert test_user.posts[0].title == "Test Post"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPostModel:
|
|
"""Test Post model functionality."""
|
|
|
|
async def test_post_creation(self, async_session, test_user):
|
|
"""Test creating a post."""
|
|
post = Post(
|
|
title="Test Post",
|
|
content="This is test content",
|
|
created_by_user_id=test_user.id
|
|
)
|
|
|
|
async_session.add(post)
|
|
await async_session.commit()
|
|
await async_session.refresh(post)
|
|
|
|
assert post.id is not None
|
|
assert post.title == "Test Post"
|
|
assert post.content == "This is test content"
|
|
assert post.created_by_user_id == test_user.id
|
|
assert post.created_at is not None
|
|
assert post.is_deleted is False
|
|
```
|
|
|
|
### Schema Tests
|
|
|
|
```python
|
|
# tests/test_schemas.py
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from src.app.schemas.user import UserCreate, UserRead, UserUpdate
|
|
from src.app.schemas.post import PostCreate, PostRead, PostUpdate
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUserSchemas:
|
|
"""Test User schema validation."""
|
|
|
|
def test_user_create_valid(self):
|
|
"""Test valid user creation schema."""
|
|
user_data = {
|
|
"name": "John Doe",
|
|
"username": "johndoe",
|
|
"email": "john@example.com",
|
|
"password": "SecurePass123!"
|
|
}
|
|
|
|
user = UserCreate(**user_data)
|
|
assert user.name == "John Doe"
|
|
assert user.username == "johndoe"
|
|
assert user.email == "john@example.com"
|
|
assert user.password == "SecurePass123!"
|
|
|
|
def test_user_create_invalid_email(self):
|
|
"""Test invalid email validation."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(
|
|
name="John Doe",
|
|
username="johndoe",
|
|
email="invalid-email",
|
|
password="SecurePass123!"
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['type'] == 'value_error' for error in errors)
|
|
|
|
def test_user_create_short_password(self):
|
|
"""Test password length validation."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(
|
|
name="John Doe",
|
|
username="johndoe",
|
|
email="john@example.com",
|
|
password="123"
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['type'] == 'value_error' for error in errors)
|
|
|
|
def test_user_update_partial(self):
|
|
"""Test partial user update."""
|
|
update_data = {"name": "Jane Doe"}
|
|
user_update = UserUpdate(**update_data)
|
|
|
|
assert user_update.name == "Jane Doe"
|
|
assert user_update.username is None
|
|
assert user_update.email is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPostSchemas:
|
|
"""Test Post schema validation."""
|
|
|
|
def test_post_create_valid(self):
|
|
"""Test valid post creation."""
|
|
post_data = {
|
|
"title": "Test Post",
|
|
"content": "This is a test post content"
|
|
}
|
|
|
|
post = PostCreate(**post_data)
|
|
assert post.title == "Test Post"
|
|
assert post.content == "This is a test post content"
|
|
|
|
def test_post_create_empty_title(self):
|
|
"""Test empty title validation."""
|
|
with pytest.raises(ValidationError):
|
|
PostCreate(
|
|
title="",
|
|
content="This is a test post content"
|
|
)
|
|
|
|
def test_post_create_long_title(self):
|
|
"""Test title length validation."""
|
|
with pytest.raises(ValidationError):
|
|
PostCreate(
|
|
title="x" * 101, # Exceeds max length
|
|
content="This is a test post content"
|
|
)
|
|
```
|
|
|
|
### CRUD Tests
|
|
|
|
```python
|
|
# tests/test_crud.py
|
|
import pytest
|
|
from src.app.crud.crud_users import crud_users
|
|
from src.app.crud.crud_posts import crud_posts
|
|
from src.app.schemas.user import UserCreate, UserUpdate
|
|
from src.app.schemas.post import PostCreate, PostUpdate
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUserCRUD:
|
|
"""Test User CRUD operations."""
|
|
|
|
async def test_create_user(self, async_session):
|
|
"""Test creating a user."""
|
|
user_data = UserCreate(
|
|
name="CRUD User",
|
|
username="cruduser",
|
|
email="crud@example.com",
|
|
password="password123"
|
|
)
|
|
|
|
user = await crud_users.create(db=async_session, object=user_data)
|
|
assert user["name"] == "CRUD User"
|
|
assert user["username"] == "cruduser"
|
|
assert user["email"] == "crud@example.com"
|
|
assert "id" in user
|
|
|
|
async def test_get_user(self, async_session, test_user):
|
|
"""Test getting a user."""
|
|
retrieved_user = await crud_users.get(
|
|
db=async_session,
|
|
id=test_user.id
|
|
)
|
|
|
|
assert retrieved_user is not None
|
|
assert retrieved_user["id"] == test_user.id
|
|
assert retrieved_user["name"] == test_user.name
|
|
assert retrieved_user["username"] == test_user.username
|
|
|
|
async def test_get_user_by_email(self, async_session, test_user):
|
|
"""Test getting a user by email."""
|
|
retrieved_user = await crud_users.get(
|
|
db=async_session,
|
|
email=test_user.email
|
|
)
|
|
|
|
assert retrieved_user is not None
|
|
assert retrieved_user["email"] == test_user.email
|
|
|
|
async def test_update_user(self, async_session, test_user):
|
|
"""Test updating a user."""
|
|
update_data = UserUpdate(name="Updated Name")
|
|
|
|
updated_user = await crud_users.update(
|
|
db=async_session,
|
|
object=update_data,
|
|
id=test_user.id
|
|
)
|
|
|
|
assert updated_user["name"] == "Updated Name"
|
|
assert updated_user["id"] == test_user.id
|
|
|
|
async def test_delete_user(self, async_session, test_user):
|
|
"""Test soft deleting a user."""
|
|
await crud_users.delete(db=async_session, id=test_user.id)
|
|
|
|
# User should be soft deleted
|
|
deleted_user = await crud_users.get(
|
|
db=async_session,
|
|
id=test_user.id,
|
|
is_deleted=True
|
|
)
|
|
|
|
assert deleted_user is not None
|
|
assert deleted_user["is_deleted"] is True
|
|
|
|
async def test_get_multi_users(self, async_session):
|
|
"""Test getting multiple users."""
|
|
# Create multiple users
|
|
for i in range(5):
|
|
user_data = UserCreate(
|
|
name=f"User {i}",
|
|
username=f"user{i}",
|
|
email=f"user{i}@example.com",
|
|
password="password123"
|
|
)
|
|
await crud_users.create(db=async_session, object=user_data)
|
|
|
|
# Get users with pagination
|
|
result = await crud_users.get_multi(
|
|
db=async_session,
|
|
offset=0,
|
|
limit=3
|
|
)
|
|
|
|
assert len(result["data"]) == 3
|
|
assert result["total_count"] == 5
|
|
assert result["has_more"] is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPostCRUD:
|
|
"""Test Post CRUD operations."""
|
|
|
|
async def test_create_post(self, async_session, test_user):
|
|
"""Test creating a post."""
|
|
post_data = PostCreate(
|
|
title="Test Post",
|
|
content="This is test content"
|
|
)
|
|
|
|
post = await crud_posts.create(
|
|
db=async_session,
|
|
object=post_data,
|
|
created_by_user_id=test_user.id
|
|
)
|
|
|
|
assert post["title"] == "Test Post"
|
|
assert post["content"] == "This is test content"
|
|
assert post["created_by_user_id"] == test_user.id
|
|
|
|
async def test_get_posts_by_user(self, async_session, test_user):
|
|
"""Test getting posts by user."""
|
|
# Create multiple posts
|
|
for i in range(3):
|
|
post_data = PostCreate(
|
|
title=f"Post {i}",
|
|
content=f"Content {i}"
|
|
)
|
|
await crud_posts.create(
|
|
db=async_session,
|
|
object=post_data,
|
|
created_by_user_id=test_user.id
|
|
)
|
|
|
|
# Get posts by user
|
|
result = await crud_posts.get_multi(
|
|
db=async_session,
|
|
created_by_user_id=test_user.id
|
|
)
|
|
|
|
assert len(result["data"]) == 3
|
|
assert result["total_count"] == 3
|
|
```
|
|
|
|
## Integration Tests
|
|
|
|
### API Endpoint Tests
|
|
|
|
```python
|
|
# tests/test_api_users.py
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestUserAPI:
|
|
"""Test User API endpoints."""
|
|
|
|
async def test_create_user(self, async_client: AsyncClient):
|
|
"""Test user creation endpoint."""
|
|
user_data = {
|
|
"name": "New User",
|
|
"username": "newuser",
|
|
"email": "new@example.com",
|
|
"password": "SecurePass123!"
|
|
}
|
|
|
|
response = await async_client.post("/api/v1/users", json=user_data)
|
|
assert response.status_code == 201
|
|
|
|
data = response.json()
|
|
assert data["name"] == "New User"
|
|
assert data["username"] == "newuser"
|
|
assert data["email"] == "new@example.com"
|
|
assert "hashed_password" not in data
|
|
assert "id" in data
|
|
|
|
async def test_create_user_duplicate_email(self, async_client: AsyncClient, test_user):
|
|
"""Test creating user with duplicate email."""
|
|
user_data = {
|
|
"name": "Duplicate User",
|
|
"username": "duplicateuser",
|
|
"email": test_user.email, # Use existing email
|
|
"password": "SecurePass123!"
|
|
}
|
|
|
|
response = await async_client.post("/api/v1/users", json=user_data)
|
|
assert response.status_code == 409 # Conflict
|
|
|
|
async def test_get_users(self, async_client: AsyncClient):
|
|
"""Test getting users list."""
|
|
response = await async_client.get("/api/v1/users")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "data" in data
|
|
assert "total_count" in data
|
|
assert "has_more" in data
|
|
assert isinstance(data["data"], list)
|
|
|
|
async def test_get_user_by_id(self, async_client: AsyncClient, test_user):
|
|
"""Test getting specific user."""
|
|
response = await async_client.get(f"/api/v1/users/{test_user.id}")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["id"] == test_user.id
|
|
assert data["name"] == test_user.name
|
|
assert data["username"] == test_user.username
|
|
|
|
async def test_get_user_not_found(self, async_client: AsyncClient):
|
|
"""Test getting non-existent user."""
|
|
response = await async_client.get("/api/v1/users/99999")
|
|
assert response.status_code == 404
|
|
|
|
async def test_update_user_authorized(self, async_client: AsyncClient, test_user, auth_headers):
|
|
"""Test updating user with proper authorization."""
|
|
update_data = {"name": "Updated Name"}
|
|
|
|
response = await async_client.patch(
|
|
f"/api/v1/users/{test_user.id}",
|
|
json=update_data,
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["name"] == "Updated Name"
|
|
assert data["id"] == test_user.id
|
|
|
|
async def test_update_user_unauthorized(self, async_client: AsyncClient, test_user):
|
|
"""Test updating user without authorization."""
|
|
update_data = {"name": "Updated Name"}
|
|
|
|
response = await async_client.patch(
|
|
f"/api/v1/users/{test_user.id}",
|
|
json=update_data
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
async def test_delete_user_superuser(self, async_client: AsyncClient, test_user, superuser_headers):
|
|
"""Test deleting user as superuser."""
|
|
response = await async_client.delete(
|
|
f"/api/v1/users/{test_user.id}",
|
|
headers=superuser_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
async def test_delete_user_forbidden(self, async_client: AsyncClient, test_user, auth_headers):
|
|
"""Test deleting user without superuser privileges."""
|
|
response = await async_client.delete(
|
|
f"/api/v1/users/{test_user.id}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestAuthAPI:
|
|
"""Test Authentication API endpoints."""
|
|
|
|
async def test_login_success(self, async_client: AsyncClient, test_user):
|
|
"""Test successful login."""
|
|
login_data = {
|
|
"username": test_user.username,
|
|
"password": "testpassword123"
|
|
}
|
|
|
|
response = await async_client.post("/api/v1/auth/login", data=login_data)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "access_token" in data
|
|
assert "refresh_token" in data
|
|
assert data["token_type"] == "bearer"
|
|
|
|
async def test_login_invalid_credentials(self, async_client: AsyncClient, test_user):
|
|
"""Test login with invalid credentials."""
|
|
login_data = {
|
|
"username": test_user.username,
|
|
"password": "wrongpassword"
|
|
}
|
|
|
|
response = await async_client.post("/api/v1/auth/login", data=login_data)
|
|
assert response.status_code == 401
|
|
|
|
async def test_get_current_user(self, async_client: AsyncClient, test_user, auth_headers):
|
|
"""Test getting current user information."""
|
|
response = await async_client.get("/api/v1/auth/me", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["id"] == test_user.id
|
|
assert data["username"] == test_user.username
|
|
|
|
async def test_refresh_token(self, async_client: AsyncClient, test_user):
|
|
"""Test token refresh."""
|
|
# First login to get refresh token
|
|
login_data = {
|
|
"username": test_user.username,
|
|
"password": "testpassword123"
|
|
}
|
|
|
|
login_response = await async_client.post("/api/v1/auth/login", data=login_data)
|
|
refresh_token = login_response.json()["refresh_token"]
|
|
|
|
# Use refresh token to get new access token
|
|
refresh_response = await async_client.post(
|
|
"/api/v1/auth/refresh",
|
|
headers={"Authorization": f"Bearer {refresh_token}"}
|
|
)
|
|
|
|
assert refresh_response.status_code == 200
|
|
data = refresh_response.json()
|
|
assert "access_token" in data
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
### Basic Test Commands
|
|
|
|
```bash
|
|
# Run all tests
|
|
uv run pytest
|
|
|
|
# Run specific test categories
|
|
uv run pytest -m unit
|
|
uv run pytest -m integration
|
|
uv run pytest -m api
|
|
|
|
# Run tests with coverage
|
|
uv run pytest --cov=src --cov-report=html
|
|
|
|
# Run tests in parallel
|
|
uv run pytest -n auto
|
|
|
|
# Run specific test file
|
|
uv run pytest tests/test_api_users.py
|
|
|
|
# Run with verbose output
|
|
uv run pytest -v
|
|
|
|
# Run tests matching pattern
|
|
uv run pytest -k "test_user"
|
|
|
|
# Run tests and stop on first failure
|
|
uv run pytest -x
|
|
|
|
# Run slow tests
|
|
uv run pytest -m slow
|
|
```
|
|
|
|
### Test Environment Setup
|
|
|
|
```bash
|
|
# Set up test database
|
|
createdb test_db
|
|
|
|
# Run tests with specific environment
|
|
ENVIRONMENT=testing uv run pytest
|
|
|
|
# Run tests with debug output
|
|
uv run pytest -s --log-cli-level=DEBUG
|
|
```
|
|
|
|
## Testing Best Practices
|
|
|
|
### Test Organization
|
|
|
|
- **Separate concerns**: Unit tests for business logic, integration tests for API endpoints
|
|
- **Use fixtures**: Create reusable test data and setup
|
|
- **Test isolation**: Each test should be independent
|
|
- **Clear naming**: Test names should describe what they're testing
|
|
|
|
### Test Data
|
|
|
|
- **Use factories**: Create test data programmatically
|
|
- **Avoid hardcoded values**: Use variables and constants
|
|
- **Clean up**: Ensure tests don't leave data behind
|
|
- **Realistic data**: Use faker or similar libraries for realistic test data
|
|
|
|
### Assertions
|
|
|
|
- **Specific assertions**: Test specific behaviors, not just "it works"
|
|
- **Multiple assertions**: Test all relevant aspects of the response
|
|
- **Error cases**: Test error conditions and edge cases
|
|
- **Performance**: Include performance tests for critical paths
|
|
|
|
### Mocking
|
|
|
|
```python
|
|
# Example of mocking external dependencies
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
@pytest.mark.unit
|
|
async def test_external_api_call():
|
|
"""Test function that calls external API."""
|
|
with patch('src.app.services.external_api.make_request') as mock_request:
|
|
mock_request.return_value = {"status": "success"}
|
|
|
|
result = await some_function_that_calls_external_api()
|
|
|
|
assert result["status"] == "success"
|
|
mock_request.assert_called_once()
|
|
```
|
|
|
|
### Continuous Integration
|
|
|
|
```yaml
|
|
# .github/workflows/test.yml
|
|
name: Tests
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:15
|
|
env:
|
|
POSTGRES_USER: test_user
|
|
POSTGRES_PASSWORD: test_pass
|
|
POSTGRES_DB: test_db
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v4
|
|
with:
|
|
python-version: 3.11
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
pip install uv
|
|
uv sync
|
|
|
|
- name: Run tests
|
|
run: uv run pytest --cov=src --cov-report=xml
|
|
|
|
- name: Upload coverage
|
|
uses: codecov/codecov-action@v3
|
|
with:
|
|
file: ./coverage.xml
|
|
```
|
|
|
|
This testing guide provides comprehensive coverage of testing strategies for the FastAPI boilerplate, ensuring reliable and maintainable code. |