| marp | true | ||
|---|---|---|---|
| theme | gaia | ||
| author | Margit ANTAL | ||
| class |
|
||
| paginate | true |
- Why testing matters
- Testing Basics
- Types of testing
- Unit tests
- Integration tests
- End-to-end (E2E) tests
- Reliability of APIs
- Testing ensures that your APIs consistently produce the correct output for a given input.
- Catching regressions (bugs) early
- Run automated tests when code changes
- Confidence in deployments
- When your tests are passing --> the new version of your API is not breaking existing functionality.
| Test Type | Scope | Dependencies | Speed |
|---|---|---|---|
| Unit Tests | Single functions/methods | Mocked | Fast |
| Integration Tests | Multiple components together | Real dependencies | Moderate |
| End-to-End Tests | Full application stack | Real services | Slow |
pytest: popular testing framework for Pythonhttpx: HTTP client for making requests in tests- FastAPI's
TestClient: built onhttpx, simplifies testing FastAPI apps
- FastAPI dependencies:
pip fastapi, uvicorn
- Test dependencies:
pip install pytest httpx
module08_testing_apis/
├─ main.py
├─ tests/
│ └─ test_main.py
main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI!"}from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, FastAPI!"}- Import
TestClientfromfastapi.testclientand the FastAPIappfrommain.py - Create a
TestClientinstance: simulates requests to the FastAPI app - Define a test function that makes a GET request to the root endpoint
- Assert that the response status code is 200 and the JSON content matches expectations
- From the project's root folder run:
python -m pytest- Result:
============================================= test session starts ==============================================
platform darwin -- Python 3.11.2, pytest-8.4.1, pluggy-1.6.0
rootdir: /Users/margitantal/PythonProjects/FASTAPI/module8_testing_apis
plugins: anyio-4.10.0
collected 1 item
tests/test_main.py . [100%]
============================================== 1 passed in 0.16s ===============================================
- Directory structure (
tests/) - Grouping by feature/module
- Naming conventions
project/
│
├── app/
│ ├── main.py
│ ├── models.py
│ ├── routes/
│ │ ├── users.py
│ │ └── items.py
│ └── dependencies.py
│
└── tests/
├── __init__.py
├── conftest.py # fixtures and setup
├── test_main.py # basic smoke tests
├── unit/
│ ├── test_models.py
│ └── test_dependencies.py
└── integration/
├── test_users.py
└── test_items.py
- Naming is critical -->
pytestdiscovers test files and functions automatically. - File naming: Prefix with
test_→ e.g.,test_users.py - Test function naming: Prefix with
test_→ e.g.,
def test_create_user_success():
...
def test_create_user_duplicate_email():
...- Describe what is being tested and under what condition
- Scope: focused on single functions
- Dependencies are mocked
fake_db = {
1: {"id": 1, "name": "Alice"},
2: {"id": 2, "name": "Bob"}
}
def get_user_from_db(user_id: int):
return fake_db.get(user_id)
@app.get("/users/{user_id}")
def read_user(user_id: int, get_user=Depends(get_user_from_db)):
user = get_user
if not user:
raise HTTPException(status_code=404, detail="User not found")
return userimport pytest
from fastapi.testclient import TestClient
from main import app, get_user_from_db
client = TestClient(app)
# --- Mock dependency ---
def mock_get_user(user_id: int):
if user_id == 1:
return {"id": 1, "name": "Mocked Alice"}
return None
# --- Override dependency ---
app.dependency_overrides[get_user_from_db] = mock_get_user# --- Tests ---
def test_read_user_success():
response = client.get("/users/1")
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "Mocked Alice"}
def test_read_user_not_found():
response = client.get("/users/999")
assert response.status_code == 404
assert response.json() == {"detail": "User not found"}-
Scope: tests functions together with their real dependencies (not isolated/mocked)
-
Database: uses a real database instance, but separate from the production database (e.g., test-specific or in-memory)
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker, Session
DATABASE_URL = "sqlite:///./app.db" # real DB for dev, overridden in tests
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
## --- Model ---
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
## Create tables
Base.metadata.create_all(bind=engine)def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/integration/{user_id}")
def read_user2(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}import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app, Base, get_db, User
# --- Setup test database ---
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)
# Create fresh schema
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
# --- Fixtures ---
@pytest.fixture
def setup_test_data():
db = TestingSessionLocal()
user = User(id=1, name="Integration Alice")
db.add(user)
db.commit()
db.refresh(user)
db.close()
return userdef test_read_user2_success(setup_test_data):
response = client.get(f"/users/integration/{setup_test_data.id}")
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "Integration Alice"}
def test_read_user2_not_found():
response = client.get("/users/integration/999")
assert response.status_code == 404
assert response.json() == {"detail": "User not found"}- Real DB: The test uses SQLite (test.db) with SQLAlchemy.
- Fresh schema: We drop and recreate tables before tests to avoid stale state.
- Fixtures:
setup_test_datainserts test data into the DB. - Overrides: We override
get_dbso the app uses our test DB instead of production. - End result: This is a true integration test — FastAPI endpoint + SQLAlchemy ORM & database working together.
- Continuous testing:
- GitHub Actions / GitLab CI
- Run tests on every push
- Test coverage
python3 -m pytest -cov
- E2E Testing Setup
- Run full app + DB + external services
- Use
docker-compose
- Tools for E2E
- pytest with live server
- Postman/Newman collections
- Playwright (for frontend + backend)
Link to homework Section: Practical Exercises: Testing APIs
- Write tests early
- Use dependency overrides
- Automate with CI/CD
- Balance between unit, integration, and E2E