Setup Testing Environment {#setup-testing}
Install Dependencies
pip install pytest pytest-asyncio httpx pytest-cov pytest-env pytest-mock
Project Structure
myapp/
├── app/
│ └── ... (aplikasi FastAPI)
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_auth.py
│ ├── test_api.py
│ └── test_db.py
├── Dockerfile
├── docker-compose.yml
├── .env.test
└── pytest.ini
Test Configuration (pytest.ini
)
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --cov=app --cov-report=term-missing
env_files =
.env.test
Unit Testing {#unit-testing}
Setup Test Database (tests/conftest.py
)
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from app.database import Base, get_db
from app.main import app
# Test database URL
SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db"
@pytest.fixture(scope="session")
def engine():
engine = create_engine(
SQLALCHEMY_TEST_DATABASE_URL,
connect_args={"check_same_thread": False}
)
Base.metadata.create_all(bind=engine)
yield engine
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def db_session(engine):
TestingSessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
@pytest.fixture(scope="function")
def client(db_session):
def override_get_db():
try:
yield db_session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()
@pytest.fixture
def test_user():
return {
"username": "testuser",
"email": "test@example.com",
"password": "testpass123"
}
Test Authentication (tests/test_auth.py
)
import pytest
from fastapi import status
def test_create_user(client, test_user):
response = client.post("/users/", json=test_user)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["email"] == test_user["email"]
assert "id" in data
def test_login(client, test_user):
# Create user first
client.post("/users/", json=test_user)
# Test login
login_data = {
"username": test_user["username"],
"password": test_user["password"]
}
response = client.post("/token", data=login_data)
assert response.status_code == status.HTTP_200_OK
assert "access_token" in response.json()
@pytest.mark.parametrize(
"email,password,status_code",
[
("wrong@example.com", "testpass123", 401),
("test@example.com", "wrongpass", 401),
(None, "testpass123", 422),
("test@example.com", None, 422),
]
)
def test_login_validation(client, test_user, email, password, status_code):
# Create user first
client.post("/users/", json=test_user)
# Test invalid login
login_data = {
"username": email,
"password": password
}
response = client.post("/token", data=login_data)
assert response.status_code == status_code
Integration Testing {#integration-testing}
Test Complete Flow (tests/test_api.py
)
def test_create_and_read_item(client, test_user):
# 1. Create user and login
client.post("/users/", json=test_user)
login_response = client.post(
"/token",
data={
"username": test_user["username"],
"password": test_user["password"]
}
)
token = login_response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
# 2. Create item
item_data = {
"title": "Test Item",
"description": "Test Description",
"price": 99.99
}
create_response = client.post(
"/items/",
json=item_data,
headers=headers
)
assert create_response.status_code == status.HTTP_201_CREATED
item_id = create_response.json()["id"]
# 3. Read item
read_response = client.get(f"/items/{item_id}")
assert read_response.status_code == status.HTTP_200_OK
assert read_response.json()["title"] == item_data["title"]
Deployment Setup {#deployment-setup}
Dockerfile
# Use official Python image
FROM python:3.9-slim
# Set working directory
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Install system dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . .
# Run migrations
RUN alembic upgrade head
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Docker Compose
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
CI/CD Pipeline {#cicd}
GitHub Actions Workflow
name: FastAPI CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-cov
- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: |
pytest --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v2
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: ${{secrets.HEROKU_APP_NAME}}
heroku_email: ${{secrets.HEROKU_EMAIL}}
dockerfile_path: "Dockerfile"
Production Best Practices {#production-practices}
1. Environment Configuration
from pydantic import BaseSettings, PostgresDsn
class Settings(BaseSettings):
PROJECT_NAME: str
ENVIRONMENT: str
DEBUG: bool = False
# Database
POSTGRES_SERVER: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
# Security
SECRET_KEY: str
ALLOWED_HOSTS: list[str]
# Performance
WORKERS_COUNT: int = 4
RELOAD: bool = False
class Config:
env_file = f".env.{ENVIRONMENT}"
settings = Settings()
2. Logging Configuration
import logging
from datetime import datetime
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
@app.middleware("http")
async def log_requests(request: Request, call_next):
start_time = datetime.now()
response = await call_next(request)
duration = datetime.now() - start_time
logger.info(
f"{request.method} {request.url.path} "
f"- Status: {response.status_code} "
f"- Duration: {duration.total_seconds():.2f}s"
)
return response
3. Health Check Endpoint
from fastapi import status
from datetime import datetime
@app.get("/health", tags=["Health Check"])
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"environment": settings.ENVIRONMENT,
"version": "1.0.0"
}
Leave a Reply