backend / web frameworks / fastapi / 06_testing_and_deployment.md

FastAPI Testing and Deployment - Interview Questions

11 min read source

FastAPI Testing and Deployment - Interview Questions

1. How do you write unit tests for FastAPI applications?

from fastapi.testclient import TestClient
from fastapi import FastAPI
import pytest
from unittest.mock import Mock, patch

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/users/{user_id}")
def read_user(user_id: int):
    return {"user_id": user_id, "name": "John Doe"}

# Basic test setup
client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"Hello": "World"}

def test_read_user():
    response = client.get("/users/1")
    assert response.status_code == 200
    assert response.json() == {"user_id": 1, "name": "John Doe"}

# Testing with pytest
@pytest.fixture
def client():
    return TestClient(app)

def test_read_root_pytest(client):
    response = client.get("/")
    assert response.status_code == 200
    data = response.json()
    assert data["Hello"] == "World"

# Testing with dependencies
from fastapi import Depends

def get_db():
    return {"users": [{"id": 1, "name": "John"}]}

@app.get("/users")
def get_users(db: dict = Depends(get_db)):
    return db["users"]

def test_get_users():
    with patch("main.get_db") as mock_db:
        mock_db.return_value = {"users": [{"id": 1, "name": "John"}]}
        response = client.get("/users")
        assert response.status_code == 200
        assert response.json() == [{"id": 1, "name": "John"}]

2. How do you test FastAPI endpoints with authentication?

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer
from fastapi.testclient import TestClient
import pytest

app = FastAPI()
security = HTTPBearer()

def get_current_user(token: str = Depends(security)):
    if token != "valid-token":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token"
        )
    return {"user_id": 1, "username": "testuser"}

@app.get("/protected")
def protected_endpoint(current_user: dict = Depends(get_current_user)):
    return {"message": "Protected data", "user": current_user}

client = TestClient(app)

def test_protected_endpoint_with_valid_token():
    headers = {"Authorization": "Bearer valid-token"}
    response = client.get("/protected", headers=headers)
    assert response.status_code == 200
    assert response.json()["message"] == "Protected data"

def test_protected_endpoint_with_invalid_token():
    headers = {"Authorization": "Bearer invalid-token"}
    response = client.get("/protected", headers=headers)
    assert response.status_code == 401

def test_protected_endpoint_without_token():
    response = client.get("/protected")
    assert response.status_code == 403

# Testing with dependency overrides
def get_test_user():
    return {"user_id": 999, "username": "testuser"}

def test_protected_endpoint_with_override():
    app.dependency_overrides[get_current_user] = get_test_user
    response = client.get("/protected")
    assert response.status_code == 200
    assert response.json()["user"]["user_id"] == 999
    app.dependency_overrides.clear()

3. How do you test database operations in FastAPI?

from fastapi import FastAPI, Depends
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from fastapi.testclient import TestClient
import pytest

app = FastAPI()

# Test database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    email = Column(String, unique=True, index=True)

Base.metadata.create_all(bind=engine)

def get_db():
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.post("/users")
def create_user(name: str, email: str, db: Session = Depends(get_db)):
    user = User(name=name, email=email)
    db.add(user)
    db.commit()
    db.refresh(user)
    return {"id": user.id, "name": user.name, "email": user.email}

@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return {"id": user.id, "name": user.name, "email": user.email}

client = TestClient(app)

@pytest.fixture
def db_session():
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=engine)

def test_create_user(db_session):
    response = client.post("/users", params={"name": "John", "email": "john@example.com"})
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "John"
    assert data["email"] == "john@example.com"

def test_get_user(db_session):
    # First create a user
    user_response = client.post("/users", params={"name": "Jane", "email": "jane@example.com"})
    user_id = user_response.json()["id"]
    
    # Then get the user
    response = client.get(f"/users/{user_id}")
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Jane"
    assert data["email"] == "jane@example.com"

def test_get_nonexistent_user(db_session):
    response = client.get("/users/999")
    assert response.status_code == 404

4. How do you test async endpoints in FastAPI?

from fastapi import FastAPI
from fastapi.testclient import TestClient
import pytest
import asyncio
import httpx

app = FastAPI()

@app.get("/async-endpoint")
async def async_endpoint():
    await asyncio.sleep(0.1)  # Simulate async operation
    return {"message": "Async response"}

@app.get("/users")
async def get_users():
    # Simulate async database query
    await asyncio.sleep(0.05)
    return [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]

# Testing with TestClient (synchronous)
client = TestClient(app)

def test_async_endpoint():
    response = client.get("/async-endpoint")
    assert response.status_code == 200
    assert response.json()["message"] == "Async response"

def test_get_users():
    response = client.get("/users")
    assert response.status_code == 200
    users = response.json()
    assert len(users) == 2
    assert users[0]["name"] == "John"

# Testing with httpx (async)
@pytest.mark.asyncio
async def test_async_endpoint_with_httpx():
    async with httpx.AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.get("/async-endpoint")
        assert response.status_code == 200
        assert response.json()["message"] == "Async response"

@pytest.mark.asyncio
async def test_get_users_with_httpx():
    async with httpx.AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.get("/users")
        assert response.status_code == 200
        users = response.json()
        assert len(users) == 2

5. How do you set up integration tests for FastAPI?

from fastapi import FastAPI
from fastapi.testclient import TestClient
import pytest
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Test configuration
TEST_DATABASE_URL = "sqlite:///./test_integration.db"

@pytest.fixture(scope="session")
def test_app():
    # Create test database
    engine = create_engine(TEST_DATABASE_URL)
    TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    
    # Create tables
    Base.metadata.create_all(bind=engine)
    
    app = FastAPI()
    
    # Override database dependency
    def get_test_db():
        db = TestingSessionLocal()
        try:
            yield db
        finally:
            db.close()
    
    app.dependency_overrides[get_db] = get_test_db
    
    yield app
    
    # Cleanup
    Base.metadata.drop_all(bind=engine)

@pytest.fixture
def client(test_app):
    return TestClient(test_app)

@pytest.fixture
def db_session(test_app):
    engine = create_engine(TEST_DATABASE_URL)
    TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

class TestUserAPI:
    def test_create_and_get_user(self, client, db_session):
        # Create user
        create_response = client.post(
            "/users",
            json={"name": "Test User", "email": "test@example.com"}
        )
        assert create_response.status_code == 200
        user_data = create_response.json()
        
        # Get user
        get_response = client.get(f"/users/{user_data['id']}")
        assert get_response.status_code == 200
        assert get_response.json()["name"] == "Test User"
    
    def test_user_validation(self, client):
        # Test invalid email
        response = client.post(
            "/users",
            json={"name": "Test User", "email": "invalid-email"}
        )
        assert response.status_code == 422
    
    def test_duplicate_email(self, client, db_session):
        # Create first user
        client.post(
            "/users",
            json={"name": "User 1", "email": "duplicate@example.com"}
        )
        
        # Try to create second user with same email
        response = client.post(
            "/users",
            json={"name": "User 2", "email": "duplicate@example.com"}
        )
        assert response.status_code == 400

6. How do you deploy a FastAPI application to production?

# main.py
from fastapi import FastAPI
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    print("Starting up...")
    yield
    # Shutdown
    print("Shutting down...")

app = FastAPI(lifespan=lifespan)

# requirements.txt
"""
fastapi==0.104.1
uvicorn[standard]==0.24.0
gunicorn==21.2.0
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
redis==5.0.1
"""

# Dockerfile
"""
FROM python:3.14-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
"""

# docker-compose.yml
"""
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/fastapi
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
  
  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=fastapi
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
"""

# Production deployment with Gunicorn
# gunicorn.conf.py
"""
bind = "0.0.0.0:8000"
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
timeout = 30
keepalive = 2
preload_app = True
"""

# systemd service file
"""
[Unit]
Description=FastAPI application
After=network.target

[Service]
User=fastapi
Group=fastapi
WorkingDirectory=/opt/fastapi
Environment="PATH=/opt/fastapi/venv/bin"
ExecStart=/opt/fastapi/venv/bin/gunicorn -c gunicorn.conf.py main:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always

[Install]
WantedBy=multi-user.target
"""

7. How do you set up CI/CD for FastAPI applications?

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pytest pytest-asyncio 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@v3
      with:
        file: ./coverage.xml

  lint:
    runs-on: ubuntu-latest
    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 flake8 black isort mypy
    
    - name: Run linters
      run: |
        flake8 app/
        black --check app/
        isort --check-only app/
        mypy app/

  security:
    runs-on: ubuntu-latest
    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 bandit safety
    
    - name: Run security checks
      run: |
        bandit -r app/
        safety check

  deploy:
    needs: [test, lint, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to production
      run: |
        echo "Deploying to production..."
        # Add your deployment commands here

8. How do you monitor and log FastAPI applications in production?

from fastapi import FastAPI, Request
import logging
import time
from prometheus_client import Counter, Histogram, generate_latest
from fastapi.responses import Response

app = FastAPI()

# Configure logging
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__)

# Prometheus metrics
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
REQUEST_LATENCY = Histogram('http_request_duration_seconds', 'HTTP request latency')

# Middleware for monitoring
@app.middleware("http")
async def monitor_requests(request: Request, call_next):
    start_time = time.time()
    
    # Log request
    logger.info(f"Request: {request.method} {request.url}")
    
    response = await call_next(request)
    
    # Calculate duration
    duration = time.time() - start_time
    
    # Update metrics
    REQUEST_COUNT.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code
    ).inc()
    REQUEST_LATENCY.observe(duration)
    
    # Log response
    logger.info(f"Response: {response.status_code} - {duration:.3f}s")
    
    return response

# Health check endpoint
@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "timestamp": time.time(),
        "version": "1.0.0"
    }

# Metrics endpoint
@app.get("/metrics")
async def metrics():
    return Response(generate_latest(), media_type="text/plain")

# Custom logging for specific endpoints
from fastapi import Depends
from pydantic import BaseModel

class UserCreate(BaseModel):
    name: str
    email: str

@app.post("/users")
async def create_user(user: UserCreate, db: Session = Depends(get_db)):
    logger.info(f"Creating user: {user.email}")
    try:
        # User creation logic
        logger.info(f"User created successfully: {user.email}")
        return {"message": "User created"}
    except Exception as e:
        logger.error(f"Error creating user {user.email}: {str(e)}")
        raise

# Structured logging
import json
from datetime import datetime

class StructuredLogger:
    def __init__(self):
        self.logger = logging.getLogger(__name__)
    
    def log_request(self, request: Request, response, duration: float):
        log_data = {
            "timestamp": datetime.utcnow().isoformat(),
            "method": request.method,
            "url": str(request.url),
            "status_code": response.status_code,
            "duration": duration,
            "user_agent": request.headers.get("user-agent"),
            "ip": request.client.host
        }
        self.logger.info(json.dumps(log_data))

structured_logger = StructuredLogger()

@app.middleware("http")
async def structured_logging(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    duration = time.time() - start_time
    
    structured_logger.log_request(request, response, duration)
    return response

9. How do you handle environment-specific configurations in FastAPI?

from pydantic import BaseSettings
from typing import Optional
import os

class Settings(BaseSettings):
    # Database settings
    database_url: str = "sqlite:///./app.db"
    database_pool_size: int = 10
    database_max_overflow: int = 20
    
    # Security settings
    secret_key: str = "your-secret-key"
    algorithm: str = "HS256"
    access_token_expire_minutes: int = 30
    
    # Redis settings
    redis_url: str = "redis://localhost:6379"
    
    # External APIs
    external_api_url: str = "https://api.external.com"
    external_api_key: Optional[str] = None
    
    # Logging
    log_level: str = "INFO"
    log_file: str = "app.log"
    
    # Environment
    environment: str = "development"
    debug: bool = False
    
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

# Environment-specific settings
class DevelopmentSettings(Settings):
    debug: bool = True
    log_level: str = "DEBUG"

class ProductionSettings(Settings):
    debug: bool = False
    log_level: str = "WARNING"
    database_pool_size: int = 20
    database_max_overflow: int = 30

class TestingSettings(Settings):
    database_url: str = "sqlite:///./test.db"
    debug: bool = True
    log_level: str = "DEBUG"

def get_settings() -> Settings:
    environment = os.getenv("ENVIRONMENT", "development")
    
    if environment == "production":
        return ProductionSettings()
    elif environment == "testing":
        return TestingSettings()
    else:
        return DevelopmentSettings()

settings = get_settings()

# Using settings in the application
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session

app = FastAPI(debug=settings.debug)

@app.get("/config")
async def get_config():
    return {
        "environment": settings.environment,
        "debug": settings.debug,
        "database_url": settings.database_url
    }

# Environment-specific database setup
def get_database_url():
    if settings.environment == "testing":
        return "sqlite:///./test.db"
    elif settings.environment == "production":
        return os.getenv("DATABASE_URL")
    else:
        return "sqlite:///./dev.db"

# Environment-specific middleware
if settings.environment == "production":
    from fastapi.middleware.trustedhost import TrustedHostMiddleware
    app.add_middleware(
        TrustedHostMiddleware,
        allowed_hosts=["yourdomain.com", "*.yourdomain.com"]
    )

10. How do you implement blue-green deployment for FastAPI?

# Blue-green deployment configuration
# docker-compose.blue.yml
"""
version: '3.8'

services:
  web-blue:
    build: .
    image: fastapi-app:blue
    ports:
      - "8001:8000"
    environment:
      - ENVIRONMENT=production
      - VERSION=blue
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  web-green:
    build: .
    image: fastapi-app:green
    ports:
      - "8002:8000"
    environment:
      - ENVIRONMENT=production
      - VERSION=green
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - web-blue
      - web-green
"""

# nginx.conf for load balancing
"""
events {
    worker_connections 1024;
}

http {
    upstream backend {
        server web-blue:8000;
        server web-green:8000 backup;
    }

    server {
        listen 80;
        
        location / {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
        
        location /health {
            proxy_pass http://backend;
        }
    }
}
"""

# Deployment script
"""
#!/bin/bash

# Blue-green deployment script

set -e

# Configuration
BLUE_PORT=8001
GREEN_PORT=8002
NGINX_PORT=80
HEALTH_CHECK_URL="http://localhost"

# Function to check if service is healthy
check_health() {
    local port=$1
    local max_attempts=30
    local attempt=1
    
    while [ $attempt -le $max_attempts ]; do
        if curl -f "$HEALTH_CHECK_URL:$port/health" > /dev/null 2>&1; then
            echo "Service on port $port is healthy"
            return 0
        fi
        
        echo "Attempt $attempt: Service on port $port is not ready yet..."
        sleep 10
        attempt=$((attempt + 1))
    done
    
    echo "Service on port $port failed health check"
    return 1
}

# Function to switch traffic
switch_traffic() {
    local active_port=$1
    local new_port=$2
    
    echo "Switching traffic from port $active_port to port $new_port"
    
    # Update nginx configuration
    sed -i "s/server web-$active_port:8000;/server web-$active_port:8000 backup;/g" nginx.conf
    sed -i "s/server web-$new_port:8000 backup;/server web-$new_port:8000;/g" nginx.conf
    
    # Reload nginx
    docker-compose exec nginx nginx -s reload
    
    echo "Traffic switched successfully"
}

# Main deployment logic
echo "Starting blue-green deployment..."

# Determine current active environment
if curl -f "$HEALTH_CHECK_URL:$BLUE_PORT/health" > /dev/null 2>&1; then
    CURRENT_ACTIVE="blue"
    NEW_ENVIRONMENT="green"
    CURRENT_PORT=$BLUE_PORT
    NEW_PORT=$GREEN_PORT
elif curl -f "$HEALTH_CHECK_URL:$GREEN_PORT/health" > /dev/null 2>&1; then
    CURRENT_ACTIVE="green"
    NEW_ENVIRONMENT="blue"
    CURRENT_PORT=$GREEN_PORT
    NEW_PORT=$BLUE_PORT
else
    echo "No active environment found, starting with blue"
    CURRENT_ACTIVE="none"
    NEW_ENVIRONMENT="blue"
    NEW_PORT=$BLUE_PORT
fi

echo "Current active: $CURRENT_ACTIVE"
echo "New environment: $NEW_ENVIRONMENT"

# Deploy new environment
echo "Deploying new environment ($NEW_ENVIRONMENT)..."
docker-compose up -d web-$NEW_ENVIRONMENT

# Wait for new environment to be healthy
echo "Waiting for new environment to be healthy..."
if ! check_health $NEW_PORT; then
    echo "New environment failed health check, rolling back..."
    docker-compose stop web-$NEW_ENVIRONMENT
    exit 1
fi

# Switch traffic if there was a previous active environment
if [ "$CURRENT_ACTIVE" != "none" ]; then
    switch_traffic $CURRENT_ACTIVE $NEW_ENVIRONMENT
    
    # Wait a bit to ensure traffic is stable
    sleep 30
    
    # Stop old environment
    echo "Stopping old environment ($CURRENT_ACTIVE)..."
    docker-compose stop web-$CURRENT_ACTIVE
else
    # First deployment, just start nginx
    echo "First deployment, starting nginx..."
    docker-compose up -d nginx
fi

echo "Blue-green deployment completed successfully!"
"""

## Interview angle

- **"How do you test a FastAPI app?"** - `TestClient` for sync tests, `httpx.AsyncClient` with ASGI transport for async ones, and `app.dependency_overrides` to swap the database and external clients for test doubles. Real dependencies come from Testcontainers at the integration layer.
- **"How do you deploy it?"** - an ASGI server (uvicorn, or gunicorn with uvicorn workers) behind a reverse proxy for TLS, static files and buffering. Worker count from cores; note that each worker is a separate process with its own connection pool, which is a common source of pool exhaustion.
- **"What belongs in the lifespan handler?"** - creating and disposing shared resources: connection pools, HTTP clients, caches. Creating an HTTP client per request throws away connection reuse and is a frequent performance bug.
- **"How do you handle migrations on deploy?"** - a separate migration step, not on application startup, and never concurrently from every replica. Use a job or an advisory lock so exactly one runs. See [../../08_databases/sql/16_alembic.md](../../08_databases/sql/16_alembic.md).