Introduction
Welcome to Module 15 of the FastAPI Complete Course. In this chapter, we dive into one of the most critical phases of professional software development: testing. If you’re building APIs for a job or a production project, writing tests is non-negotiable. Tests ensure your code works as expected, prevent regressions, and give you the confidence to refactor or add new features.
In this tutorial, we will cover the essential testing strategies for FastAPI applications. You will learn how to write unit tests, use Pytest (the de facto Python testing framework), test your API endpoints with TestClient, mock your database, and measure test coverage. By the end of this chapter, you will be able to write robust tests for any FastAPI project, making you a more reliable and job-ready developer.
Unit Testing
Unit testing focuses on testing individual components of your code in isolation. In FastAPI, this often means testing your service functions, helper utilities, or business logic without involving the database or network calls.
What Makes a Good Unit Test?
- Isolated: It does not depend on external systems (databases, APIs, file systems).
- Fast: It runs in milliseconds.
- Repeatable: It produces the same result every time.
- Readable: It clearly describes what is being tested.
Example: Testing a Simple Utility Function
Let’s say we have a utility function that calculates a discount for a user.
# app/utils.py
def calculate_discount(price: float, user_tier: str) -> float:
if user_tier == "gold":
return price * 0.8
elif user_tier == "silver":
return price * 0.9
else:
return price
Now, we write a unit test for it.
# tests/test_utils.py
from app.utils import calculate_discount
def test_calculate_discount_gold():
result = calculate_discount(100.0, "gold")
assert result == 80.0
def test_calculate_discount_silver():
result = calculate_discount(100.0, "silver")
assert result == 90.0
def test_calculate_discount_basic():
result = calculate_discount(100.0, "basic")
assert result == 100.0
Explanation: Each test function calls calculate_discount with specific inputs and uses assert to verify the output. These tests are fast and isolated—no database or server needed.
Pytest Basics
Pytest is the most popular testing framework for Python. It makes writing and running tests simple and powerful. FastAPI’s own testing utilities are built on top of Pytest.
Installing Pytest
pip install pytest
Key Pytest Features
- Auto-discovery: Pytest automatically finds test files named
test_*.pyor*_test.py. - Assertions: Use Python’s built-in
assertstatement—no need for special methods. - Fixtures: Reusable setup and teardown logic.
- Parametrization: Run the same test with different inputs.
Running Tests
# Run all tests in the tests/ directory
pytest tests/
# Run with verbose output
pytest -v tests/
# Run a specific test file
pytest tests/test_utils.py
# Run a specific test function
pytest tests/test_utils.py::test_calculate_discount_gold
Example: Using Parametrization
Instead of writing three separate tests, we can use @pytest.mark.parametrize.
import pytest
from app.utils import calculate_discount
@pytest.mark.parametrize("price, tier, expected", [
(100.0, "gold", 80.0),
(100.0, "silver", 90.0),
(100.0, "basic", 100.0),
(0.0, "gold", 0.0),
])
def test_calculate_discount(price, tier, expected):
assert calculate_discount(price, tier) == expected
Why this matters: Parametrization reduces code duplication and makes it easy to add new test cases.
API Testing
API testing verifies that your FastAPI endpoints work correctly. FastAPI provides a TestClient class (based on Starlette’s test client) that lets you simulate HTTP requests without running a live server.
Setting Up TestClient
First, install httpx (TestClient requires it).
pip install httpx
Basic API Test Example
Assume we have a simple FastAPI app.
# app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
Now, let’s test these endpoints.
# tests/test_api.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_read_item():
response = client.get("/items/42?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 42, "q": "test"}
def test_read_item_no_query():
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": None}
Explanation:
- We create a
TestClientinstance, passing our FastAPI app. - We call
client.get()to simulate GET requests. - We check the status code and JSON response.
Testing POST, PUT, DELETE
Here’s an example with a POST endpoint.
# app/main.py (add this)
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
def create_item(item: Item):
return {"name": item.name, "price": item.price}
# tests/test_api.py (add this)
def test_create_item():
response = client.post("/items/", json={"name": "Laptop", "price": 999.99})
assert response.status_code == 200
data = response.json()
assert data["name"] == "Laptop"
assert data["price"] == 999.99
Key point: Use the json parameter to send JSON data in the request body.
Mocking Database
In real-world applications, your endpoints often interact with a database. During testing, you should mock the database to avoid side effects and speed up tests. Mocking replaces real dependencies with fake objects that behave predictably.
Why Mock?
- Tests run without a real database.
- You can simulate error conditions (e.g., database timeout).
- Tests become deterministic and fast.
Example: A Service with Database Dependency
Let’s create a simple user service that depends on a database session.
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# app/models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
email = Column(String)
# app/services.py
from sqlalchemy.orm import Session
from app.models import User
def get_user_by_email(db: Session, email: str):
return db.query(User).filter(User.email == email).first()
# app/main.py (add this)
from fastapi import Depends
from app.database import SessionLocal
from app.services import get_user_by_email
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{email}")
def read_user(email: str, db: Session = Depends(get_db)):
user = get_user_by_email(db, email)
if user is None:
return {"error": "User not found"}
return {"id": user.id, "name": user.name, "email": user.email}
Mocking the Database with unittest.mock
We’ll use Python’s unittest.mock to mock the database session and the service function.
# tests/test_mocked_api.py
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
from app.main import app
client = TestClient(app)
def test_read_user_found():
# Create a mock user object
mock_user = MagicMock()
mock_user.id = 1
mock_user.name = "Alice"
mock_user.email = "alice@example.com"
# Patch the service function to return the mock user
with patch("app.services.get_user_by_email", return_value=mock_user):
response = client.get("/users/alice@example.com")
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "Alice", "email": "alice@example.com"}
def test_read_user_not_found():
# Patch the service function to return None
with patch("app.services.get_user_by_email", return_value=None):
response = client.get("/users/nonexistent@example.com")
assert response.status_code == 200
assert response.json() == {"error": "User not found"}
Explanation:
patch("app.services.get_user_by_email", return_value=...)replaces the real function with a mock that returns a predefined value.- The mock object simulates a database user without hitting the actual database.
- We test both the “found” and “not found” scenarios.
Using Fixtures for Reusable Mocks
Fixtures make your tests cleaner and more reusable.
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def mock_db_user():
user = MagicMock()
user.id = 1
user.name = "Bob"
user.email = "bob@example.com"
return user
# tests/test_with_fixtures.py
from unittest.mock import patch
def test_read_user_found(client, mock_db_user):
with patch("app.services.get_user_by_email", return_value=mock_db_user):
response = client.get("/users/bob@example.com")
assert response.status_code == 200
assert response.json()["name"] == "Bob"
Best practice: Keep fixtures in conftest.py so they are available to all test files in the same directory.
Test Coverage
Test coverage measures how much of your code is executed by your tests. It helps identify untested parts of your application. However, 100% coverage does not guarantee bug-free code—it only tells you which lines were run.
Installing Coverage.py
pip install pytest-cov
Running Coverage with Pytest
# Run tests and show coverage report in terminal
pytest --cov=app tests/
# Generate an HTML report for visual inspection
pytest --cov=app --cov-report=html tests/
The HTML report will be in the htmlcov/ directory. Open index.html in your browser to see which lines are covered (green) and which are not (red).
Setting a Coverage Threshold
You can enforce a minimum coverage percentage in your pyproject.toml or setup.cfg.
# setup.cfg
[tool:pytest]
addopts = --cov=app --cov-fail-under=80
Now, if coverage drops below 80%, the test suite will fail.
What to Aim For?
- 80-90% is a good target for most projects.
- Focus on testing business logic and API endpoints.
- Don’t obsess over covering trivial getters/setters or generated code.
Common Mistakes
- Testing with a real database: This makes tests slow and fragile. Always mock external dependencies.
- Not using fixtures: Repeating setup code in every test leads to maintenance nightmares.
- Ignoring edge cases: Test empty inputs, invalid data, missing parameters, and error responses.
- Over-mocking: Mocking too many things can hide real integration issues. Test the actual logic where possible.
- Forgetting to check status codes: Always assert the HTTP status code, not just the response body.
- Running tests against a live server: Use
TestClientinstead of startinguvicornmanually.
Practice Task
Now it’s your turn. Build and test a small FastAPI application with the following requirements:
- Create a FastAPI app with two endpoints:
POST /tasks/: Accepts a JSON body withtitle(string) andcompleted(boolean, default False). Returns the created task with anid(integer).GET /tasks/{task_id}: Returns a task by ID. Return 404 if not found.
- Use a service layer (e.g.,
app/services.py) that stores tasks in a Python dictionary (simulating a database). - Write the following tests:
- Test creating a task.
- Test retrieving an existing task.
- Test retrieving a non-existent task (expect 404).
- Mock the service layer using
unittest.mock.patch.
- Run coverage and ensure your tests cover at least 90% of your application code.
Hint: Use pytest --cov=app --cov-report=term-missing to see which lines are missing coverage.
Summary
In this module, you learned the foundations of testing FastAPI applications:
- Unit testing for isolated logic.
- Pytest basics including parametrization and fixtures.
- API testing with
TestClient. - Mocking databases to keep tests fast and reliable.
- Test coverage to measure and improve your test suite.
Testing is a skill that separates professional developers from amateurs. Master it, and you’ll write code that is robust, maintainable, and ready for production.
FAQs
1. What is the difference between unit testing and API testing?
Unit testing tests individual functions or methods in isolation, often mocking external dependencies. API testing (or integration testing) tests the full HTTP endpoint, including routing, request parsing, and response generation.
2. Do I need to test every single endpoint?
Yes, in a professional setting, every endpoint should have at least one test for the happy path and one for the error path (e.g., 404, 422 validation errors).
3. Can I use pytest fixtures to create test data?
Absolutely! Fixtures are the recommended way to set up test data, mock dependencies, and create reusable components like a TestClient instance.
4. How do I test authentication in FastAPI?
You can mock the dependency that extracts the current user. For example, if you have a get_current_user dependency, patch it to return a fake user object during tests.
5. What if my tests are slow?
Slow tests are usually caused by real I/O (database, network). Mock those dependencies. If you must test against a real database, use an in-memory SQLite database or a test container.
Congratulations on completing Module 15! You are now equipped to write professional-grade tests for FastAPI. In Module 16: Deployment and Production, you’ll learn how to containerize your app with Docker, deploy to cloud platforms, and set up CI/CD pipelines. See you there!
Additional Practical Example
Let’s build a more comprehensive test suite for a real-world API endpoint that handles user registration. This example will demonstrate how to test database interactions, validation, and error handling in a single flow.
# test_user_registration.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
from app.models import User
from app.schemas import UserCreate
# 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)
# Override dependency
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup_database():
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
def test_register_user_success():
"""Test successful user registration with valid data."""
response = client.post(
"/register",
json={
"username": "testuser",
"email": "test@example.com",
"password": "SecurePass123!",
"full_name": "Test User"
}
)
assert response.status_code == 201
data = response.json()
assert data["username"] == "testuser"
assert data["email"] == "test@example.com"
assert "id" in data
assert "password" not in data # Password should never be returned
def test_register_user_duplicate_email():
"""Test that duplicate email raises 400 error."""
# First registration
client.post(
"/register",
json={
"username": "user1",
"email": "duplicate@example.com",
"password": "SecurePass123!"
}
)
# Second registration with same email
response = client.post(
"/register",
json={
"username": "user2",
"email": "duplicate@example.com",
"password": "AnotherPass456!"
}
)
assert response.status_code == 400
assert "Email already registered" in response.json()["detail"]
def test_register_user_weak_password():
"""Test that weak passwords are rejected."""
response = client.post(
"/register",
json={
"username": "weakuser",
"email": "weak@example.com",
"password": "123",
"full_name": "Weak Password User"
}
)
assert response.status_code == 422 # Validation error
errors = response.json()["detail"]
assert any("password" in error["loc"] for error in errors)
def test_register_user_missing_fields():
"""Test that missing required fields return validation errors."""
response = client.post(
"/register",
json={"username": "incomplete"}
)
assert response.status_code == 422
errors = response.json()["detail"]
missing_fields = ["email", "password"]
for field in missing_fields:
assert any(field in error["loc"] for error in errors)
def test_register_user_invalid_email():
"""Test that invalid email format is rejected."""
response = client.post(
"/register",
json={
"username": "bademail",
"email": "not-an-email",
"password": "SecurePass123!"
}
)
assert response.status_code == 422
assert any("email" in error["loc"] for error in response.json()["detail"])
This example demonstrates several important testing patterns:
- Database setup/teardown: The
setup_databasefixture creates fresh tables before each test and drops them after, ensuring test isolation. - Dependency override: We replace the production database with a test SQLite database to avoid affecting real data.
- Comprehensive validation: Tests cover success cases, duplicate data, weak passwords, missing fields, and invalid formats.
- Security checking: The success test verifies that passwords are never returned in the response.
When running these tests, you’ll see clear output indicating which test passed or failed. The autouse=True fixture ensures the database is always clean, preventing test pollution.
Class-Based Implementation Example
While function-based tests are common, FastAPI also supports class-based test organization. This approach can be beneficial when you have related tests that share setup logic or when you want to group tests logically.
# test_todo_api.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db
from app.models import Todo, User
from app.schemas import TodoCreate
class TestTodoAPI:
"""Class-based test suite for Todo API endpoints."""
@pytest.fixture(autouse=True)
def setup_method(self, test_db):
"""Setup runs before each test method."""
self.client = TestClient(app)
self.db = test_db
# Create a test user and get token
self.test_user = self._create_test_user()
self.token = self._get_token()
self.headers = {"Authorization": f"Bearer {self.token}"}
def _create_test_user(self):
"""Helper to create a test user."""
response = self.client.post(
"/register",
json={
"username": "testuser",
"email": "test@example.com",
"password": "TestPass123!"
}
)
return response.json()
def _get_token(self):
"""Helper to get authentication token."""
response = self.client.post(
"/token",
data={
"username": "testuser",
"password": "TestPass123!"
}
)
return response.json()["access_token"]
def test_create_todo(self):
"""Test creating a new todo item."""
response = self.client.post(
"/todos/",
json={
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"priority": 3
},
headers=self.headers
)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Buy groceries"
assert data["completed"] == False
assert data["owner_id"] == self.test_user["id"]
def test_get_todos_empty(self):
"""Test getting todos when none exist."""
response = self.client.get("/todos/", headers=self.headers)
assert response.status_code == 200
assert response.json() == []
def test_get_todos_with_data(self):
"""Test getting todos after creating some."""
# Create multiple todos
todos_data = [
{"title": "Task 1", "priority": 1},
{"title": "Task 2", "priority": 2},
{"title": "Task 3", "priority": 3}
]
for todo in todos_data:
self.client.post("/todos/", json=todo, headers=self.headers)
response = self.client.get("/todos/", headers=self.headers)
assert response.status_code == 200
todos = response.json()
assert len(todos) == 3
# Verify ordering by priority
assert todos[0]["priority"] <= todos[1]["priority"] <= todos[2]["priority"]
def test_update_todo(self):
"""Test updating an existing todo."""
# Create a todo first
create_resp = self.client.post(
"/todos/",
json={"title": "Original Title", "priority": 1},
headers=self.headers
)
todo_id = create_resp.json()["id"]
# Update it
update_resp = self.client.put(
f"/todos/{todo_id}",
json={
"title": "Updated Title",
"completed": True,
"priority": 5
},
headers=self.headers
)
assert update_resp.status_code == 200
updated = update_resp.json()
assert updated["title"] == "Updated Title"
assert updated["completed"] == True
def test_delete_todo(self):
"""Test deleting a todo."""
# Create a todo
create_resp = self.client.post(
"/todos/",
json={"title": "To Delete", "priority": 1},
headers=self.headers
)
todo_id = create_resp.json()["id"]
# Delete it
delete_resp = self.client.delete(f"/todos/{todo_id}", headers=self.headers)
assert delete_resp.status_code == 204
# Verify it's gone
get_resp = self.client.get("/todos/", headers=self.headers)
assert len(get_resp.json()) == 0
def test_todo_not_found(self):
"""Test accessing a non-existent todo."""
response = self.client.get("/todos/99999", headers=self.headers)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_unauthorized_access(self):
"""Test that endpoints require authentication."""
response = self.client.get("/todos/")
assert response.status_code == 401
assert "not authenticated" in response.json()["detail"].lower()
def test_todo_ownership(self):
"""Test that users can only access their own todos."""
# Create a todo as testuser
create_resp = self.client.post(
"/todos/",
json={"title": "Private Todo", "priority": 1},
headers=self.headers
)
todo_id = create_resp.json()["id"]
# Try to access as different user
# Create second user
self.client.post(
"/register",
json={
"username": "otheruser",
"email": "other@example.com",
"password": "OtherPass123!"
}
)
other_token = self.client.post(
"/token",
data={"username": "otheruser", "password": "OtherPass123!"}
).json()["access_token"]
other_headers = {"Authorization": f"Bearer {other_token}"}
response = self.client.get(f"/todos/{todo_id}", headers=other_headers)
assert response.status_code == 403
assert "not authorized" in response.json()["detail"].lower()
Class-based testing offers several advantages:
- Shared state: The
setup_methodfixture runs before every test, providing a fresh test user and authentication token. - Helper methods: Private methods like
_create_test_userand_get_tokenreduce code duplication. - Logical grouping: All todo-related tests are contained in one class, making the test suite more organized.
- Clear test flow: Each test method follows a clear arrange-act-assert pattern, making tests easy to read and maintain.
Note that class-based tests still use pytest fixtures and can be run with the same commands as function-based tests. The autouse=True parameter ensures the setup runs automatically for each test method.
Hands-On Practice Task
Now it’s time to apply what you’ve learned. Complete the following practice task to build your testing skills:
Task: Build a Test Suite for a Book Library API
You have a FastAPI application that manages a book library with the following endpoints:
POST /books/– Add a new book (requires: title, author, isbn, published_year)GET /books/– List all books (supports query parameters: author, year)GET /books/{book_id}– Get a specific bookPUT /books/{book_id}– Update a bookDELETE /books/{book_id}– Delete a bookPOST /books/{book_id}/checkout– Check out a book (requires authentication)POST /books/{book_id}/return– Return a checked-out book
Requirements:
- Create a test file
test_book_library.pywith at least 10 test functions - Include tests for:
- Successful book creation with valid data
- Book creation with missing required fields (should return 422)
- Book creation with duplicate ISBN (should return 400)
- Listing books with no filters (should return all)
- Listing books filtered by author
- Listing books filtered by year
- Getting a specific book by ID
- Getting a non-existent book (should return 404)
- Updating a book with valid data
- Deleting a book
- Checking out a book (requires authentication)
- Returning a checked-out book
- Checking out an already checked-out book (should return 400)
- Use a test database with SQLite for isolation
- Implement proper fixtures for database setup and teardown
- Test both success and error scenarios
- Include at least one parametrized test
Starter Code:
# test_book_library.py - Starter template
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db, Base
from app.models import Book, User
# Your code here:
# 1. Setup test database
# 2. Override dependencies
# 3. Create fixtures
# 4. Write test functions
def test_create_book_success():
"""Test creating a book with valid data."""
pass # Implement this
def test_create_book_missing_fields():
"""Test that missing fields return validation error."""
pass # Implement this
def test_create_book_duplicate_isbn():
"""Test that duplicate ISBN returns error."""
pass # Implement this
# Continue with remaining tests...
@pytest.mark.parametrize("author,expected_count", [
("J.K. Rowling", 2),
("George Orwell", 1),
("Unknown Author", 0),
])
def test_list_books_by_author(author, expected_count):
"""Test filtering books by author."""
pass # Implement this
Expected Output:
When you run your tests with pytest -v test_book_library.py, you should see output similar to:
test_book_library.py::test_create_book_success PASSED
test_book_library.py::test_create_book_missing_fields PASSED
test_book_library.py::test_create_book_duplicate_isbn PASSED
test_book_library.py::test_list_books_no_filter PASSED
test_book_library.py::test_list_books_by_author[George Orwell-1] PASSED
test_book_library.py::test_list_books_by_author[Unknown Author-0] PASSED
test_book_library.py::test_get_book_by_id PASSED
test_book_library.py::test_get_nonexistent_book PASSED
test_book_library.py::test_update_book PASSED
test_book_library.py::test_delete_book PASSED
test_book_library.py::test_checkout_book PASSED
test_book_library.py::test_return_book PASSED
test_book_library.py::test_checkout_already_checked_out PASSED
============================= 13 passed in 2.34s =============================
Bonus Challenge:
Add tests for:
- Rate limiting (if implemented)
- Pagination (if the API supports it)
- Sorting books by different fields
- Batch operations (if available)
Common Interview Questions
Testing is a frequent topic in FastAPI interviews. Here are common questions you might encounter, along with detailed answers:
Q1: How do you test FastAPI endpoints that require authentication?
Answer: You need to simulate the authentication flow in your tests. The most common approach is to create a test user, obtain an authentication token (JWT or session cookie), and include it in the request headers. Here’s an example:
def test_authenticated_endpoint():
# Create test user
client.post("/register", json={"username": "test", "password": "pass"})
# Get token
token_resp = client.post("/token", data={"username": "test", "password": "pass"})
token = token_resp.json()["access_token"]
# Use token in subsequent requests
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/protected-endpoint", headers=headers)
assert response.status_code == 200
Q2: How do you handle database dependencies in tests?
Answer: FastAPI’s dependency injection system makes this straightforward. Override the database dependency with a test database using app.dependency_overrides. Use SQLite for testing (it’s fast and requires no setup), and create/drop tables for each test to ensure isolation. Always use fixtures to manage the database lifecycle.
Q3: What’s the difference between unit tests and integration tests for FastAPI?
Answer: Unit tests focus on testing individual functions or methods in isolation, mocking external dependencies like databases. Integration tests test the full request-response cycle, including the database, middleware, and authentication. For FastAPI, most tests are integration tests using TestClient because they provide more confidence that the API works end-to-end. However, you should still write unit tests for complex business logic that doesn’t depend on the framework.
Q4: How do you test async endpoints?
Answer: FastAPI’s TestClient handles async endpoints automatically in most cases. However, if you need to test async functions directly, use pytest-asyncio:
@pytest.mark.asyncio
async def test_async_function():
result = await my_async_function()
assert result == expected_value
Q5: How do you test file uploads?
Answer: Use the TestClient’s files parameter to simulate file uploads:
def test_file_upload():
file_content = b"test file content"
response = client.post(
"/upload",
files={"file": ("test.txt", file_content, "text/plain")}
)
assert response.status_code == 200
Q6: How do you test WebSocket endpoints?
Answer: Use the TestClient’s WebSocket context manager:
def test_websocket():
with client.websocket_connect("/ws") as websocket:
websocket.send_text("Hello")
data = websocket.receive_text()
assert data == "Hello"
Q7: What is test coverage and why is it important?
Answer: Test coverage measures what percentage of your code is executed during tests. While 100% coverage doesn’t guarantee bug-free code, it helps identify untested code paths. Use tools like pytest-cov to measure coverage:
pytest --cov=app test_book_library.py
Q8: How do you handle external API calls in tests?
Answer: Mock external API calls using libraries like unittest.mock or httpx-mock. This prevents tests from depending on external services and makes them faster and more reliable:
from unittest.mock import patch
def test_external_api_call():
with patch("app.services.external_api.get_data") as mock_get:
mock_get.return_value = {"key": "value"}
response = client.get("/endpoint-that-calls-external-api")
assert response.status_code == 200
Q9: How do you test error handling in FastAPI?
Answer: Test both validation errors (422) and custom HTTP exceptions. For validation errors, send invalid data and check the response structure. For custom exceptions, trigger the error condition and verify the status code and error message:
def test_custom_exception():
response = client.get("/items/0") # Assuming item 0 doesn't exist
assert response.status_code == 404
assert response.json()["detail"] == "Item not found"
Q10: What are best practices for organizing test files in a FastAPI project?
Answer: Follow these best practices:
- Place tests in a
tests/directory at the project root - Mirror the application structure (e.g.,
tests/test_routers/test_users.py) - Use descriptive test function names that explain what’s being tested
- Group related tests using classes or separate test files
- Use conftest.py for shared fixtures
- Keep tests independent and idempotent
- Run tests with
pytest -v --tb=shortfor clear output
Mastering these testing concepts will make you a more effective FastAPI developer and prepare you for technical interviews. Remember that good tests are an investment that pays off through fewer bugs and faster development cycles.
