FastAPI Integration Testing
Introduction
Integration testing is a crucial aspect of building reliable FastAPI applications. Unlike unit tests that focus on individual components in isolation, integration tests verify that different parts of your application work correctly when combined. In FastAPI applications, integration tests typically involve testing API endpoints with real HTTP requests and verifying that the entire request-response flow works as expected.
In this tutorial, we'll explore how to write effective integration tests for FastAPI applications using pytest
and FastAPI's built-in testing tools. You'll learn to simulate HTTP requests, test database interactions, and ensure your API endpoints function correctly as a complete system.
Prerequisites
Before diving into integration testing, make sure you have:
- Basic knowledge of FastAPI
- Understanding of Python testing concepts
- Familiarity with pytest
- A FastAPI application to test
If you don't have these yet, consider reviewing the basics of FastAPI and Python testing first.
Setting Up Your Testing Environment
Let's start by setting up the necessary tools for integration testing FastAPI applications.
Required Packages
pip install fastapi pytest pytest-asyncio httpx
pytest
: Python testing frameworkpytest-asyncio
: Support for testing async functionshttpx
: HTTP client for making requests to your FastAPI application
Basic Project Structure
A typical FastAPI project structure for integration testing might look like:
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ ├── routers/
│ └── dependencies.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_integration.py
└── requirements.txt
Creating a Test Client
FastAPI provides a TestClient
that allows you to make HTTP requests to your application without starting a real server. Let's set this up in a conftest.py
file to make it available to all tests:
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
with TestClient(app) as client:
yield client
This fixture creates a test client that you can use to interact with your FastAPI application during tests.
Your First Integration Test
Let's write a simple integration test for a FastAPI endpoint:
# tests/test_integration.py
def test_read_main(client):
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
This test verifies that when you make a GET request to the root path (/
), the API returns a 200 status code and the expected JSON response.
Testing CRUD Operations
Now, let's write more comprehensive integration tests for a REST API with CRUD operations. We'll use a simple "items" API as an example:
# tests/test_integration.py
def test_create_item(client):
response = client.post(
"/items/",
json={"name": "Test Item", "description": "Test Description", "price": 10.5}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Item"
assert data["price"] == 10.5
assert "id" in data
return data["id"]
def test_read_item(client):
# First, create an item to read
item_id = test_create_item(client)
# Then retrieve it
response = client.get(f"/items/{item_id}")
assert response.status_code == 200
data = response.json()
assert data["name"] == "Test Item"
assert data["id"] == item_id
def test_update_item(client):
# First, create an item to update
item_id = test_create_item(client)
# Then update it
response = client.put(
f"/items/{item_id}",
json={"name": "Updated Item", "description": "Updated Description", "price": 20.0}
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Item"
assert data["price"] == 20.0
def test_delete_item(client):
# First, create an item to delete
item_id = test_create_item(client)
# Then delete it
response = client.delete(f"/items/{item_id}")
assert response.status_code == 204
# Verify it's gone
response = client.get(f"/items/{item_id}")
assert response.status_code == 404
Testing with Database Dependencies
Integration tests for FastAPI applications often need to interact with databases. Let's see how to set up integration tests with a database:
Setting Up a Test Database
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.main import app
from app.database import Base, get_db
# Create a test database in memory
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def db():
# Create the database
Base.metadata.create_all(bind=engine)
# Create a database session
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Clean up tables after the test
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(db):
# Override the get_db dependency
def override_get_db():
try:
yield db
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
# Clean up the override after the test
app.dependency_overrides = {}
This setup:
- Creates an in-memory SQLite database for testing
- Sets up a database fixture that creates and cleans up tables for each test
- Overrides the application's database dependency to use our test database
Testing Database Operations
Now let's test endpoints that interact with the database:
# tests/test_integration.py
def test_create_user(client):
response = client.post(
"/users/",
json={"email": "[email protected]", "password": "password123"}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "[email protected]"
assert "id" in data
def test_get_users(client):
# Create some test users first
client.post("/users/", json={"email": "[email protected]", "password": "password123"})
client.post("/users/", json={"email": "[email protected]", "password": "password123"})
# Test getting all users
response = client.get("/users/")
assert response.status_code == 200
data = response.json()
assert len(data) == 2
assert data[0]["email"] == "[email protected]"
assert data[1]["email"] == "[email protected]"
Testing Authentication and Protected Routes
Integration tests should also verify that authentication and authorization work correctly:
# tests/test_integration.py
def test_login_and_protected_route(client):
# First create a user
client.post(
"/users/",
json={"email": "[email protected]", "password": "securepassword"}
)
# Login to get access token
login_response = client.post(
"/token",
data={"username": "[email protected]", "password": "securepassword"}
)
assert login_response.status_code == 200
token_data = login_response.json()
assert "access_token" in token_data
access_token = token_data["access_token"]
# Access protected route with token
headers = {"Authorization": f"Bearer {access_token}"}
protected_response = client.get("/users/me", headers=headers)
assert protected_response.status_code == 200
user_data = protected_response.json()
assert user_data["email"] == "[email protected]"
# Try accessing without token (should fail)
no_auth_response = client.get("/users/me")
assert no_auth_response.status_code == 401
Testing File Uploads
If your API supports file uploads, you can test them like this:
# tests/test_integration.py
def test_file_upload(client):
# Create a test file
test_file_content = b"This is a test file content"
files = {"file": ("test.txt", test_file_content, "text/plain")}
# Upload the file
response = client.post("/uploadfile/", files=files)
assert response.status_code == 200
data = response.json()
assert data["filename"] == "test.txt"
assert data["content_type"] == "text/plain"
assert "file_size" in data
Testing Error Handling
It's important to test how your API handles errors:
# tests/test_integration.py
def test_not_found_error(client):
response = client.get("/nonexistent-endpoint")
assert response.status_code == 404
def test_validation_error(client):
response = client.post(
"/items/",
json={"name": "Invalid Item", "price": "not-a-number"} # price should be a number
)
assert response.status_code == 422 # Unprocessable Entity
error_data = response.json()
assert "detail" in error_data
Real-World Example: Testing a Blog API
Let's create a comprehensive integration test for a blog API:
# tests/test_blog_integration.py
import pytest
@pytest.fixture
def user_token(client):
# Create a test user
client.post(
"/users/",
json={"email": "[email protected]", "password": "blogpassword"}
)
# Login to get token
response = client.post(
"/token",
data={"username": "[email protected]", "password": "blogpassword"}
)
return response.json()["access_token"]
def test_blog_workflow(client, user_token):
headers = {"Authorization": f"Bearer {user_token}"}
# 1. Create a new blog post
create_response = client.post(
"/posts/",
headers=headers,
json={
"title": "Integration Testing with FastAPI",
"content": "This is a test blog post about integration testing...",
"published": True
}
)
assert create_response.status_code == 201
post_data = create_response.json()
post_id = post_data["id"]
# 2. Get the created post
get_response = client.get(f"/posts/{post_id}")
assert get_response.status_code == 200
assert get_response.json()["title"] == "Integration Testing with FastAPI"
# 3. Update the post
update_response = client.put(
f"/posts/{post_id}",
headers=headers,
json={
"title": "Updated: Integration Testing with FastAPI",
"content": "This content has been updated",
"published": True
}
)
assert update_response.status_code == 200
assert update_response.json()["title"] == "Updated: Integration Testing with FastAPI"
# 4. Add a comment to the post
comment_response = client.post(
f"/posts/{post_id}/comments",
headers=headers,
json={"content": "Great article about testing!"}
)
assert comment_response.status_code == 201
comment_id = comment_response.json()["id"]
# 5. Get all comments for the post
comments_response = client.get(f"/posts/{post_id}/comments")
assert comments_response.status_code == 200
comments = comments_response.json()
assert len(comments) == 1
assert comments[0]["content"] == "Great article about testing!"
# 6. Delete the comment
delete_comment_response = client.delete(
f"/comments/{comment_id}",
headers=headers
)
assert delete_comment_response.status_code == 204
# 7. Delete the post
delete_post_response = client.delete(
f"/posts/{post_id}",
headers=headers
)
assert delete_post_response.status_code == 204
# 8. Verify the post is gone
get_deleted_response = client.get(f"/posts/{post_id}")
assert get_deleted_response.status_code == 404
This test walks through a complete workflow for a blog API, testing the creation, reading, updating, and deletion of posts and comments.
Best Practices for FastAPI Integration Testing
-
Isolate tests: Each test should be independent and not rely on the state from other tests.
-
Use fixtures efficiently: Create fixtures for common setup tasks like authentication or data creation.
-
Test the happy path and edge cases: Don't just test when everything works - test error conditions too.
-
Clean up after tests: Ensure your tests clean up any resources they create, especially database records.
-
Use parameterized tests for testing similar functionality with different inputs:
@pytest.mark.parametrize("item_data,expected_status_code", [
({"name": "Valid Item", "price": 10.5}, 201),
({"name": "Invalid Item", "price": -10.5}, 422), # negative price
({"price": 10.5}, 422), # missing name
])
def test_create_item_validation(client, item_data, expected_status_code):
response = client.post("/items/", json=item_data)
assert response.status_code == expected_status_code
-
Use proper assertions: Be specific about what you're testing and provide clear error messages.
-
Test middleware and dependencies: Don't forget to test application-wide features.
Running Integration Tests
To run your tests with pytest, use the following command:
pytest tests/test_integration.py -v
Add the -v
flag for verbose output, which shows the name of each test as it runs.
Summary
Integration testing is essential for ensuring your FastAPI application works correctly as a complete system. In this tutorial, we've covered:
- Setting up a testing environment for FastAPI
- Creating and using a test client
- Writing tests for CRUD operations
- Testing database interactions
- Testing authentication and protected routes
- File upload testing
- Error handling tests
- Best practices for integration testing
By implementing comprehensive integration tests, you can be confident that your FastAPI application will behave as expected when deployed to production.
Additional Resources
Exercises
- Create integration tests for a FastAPI application with at least three different endpoints.
- Write tests for both successful operations and error cases.
- Implement a test that checks your API's rate limiting functionality.
- Create a test that verifies file upload functionality with different file types.
- Write an integration test for an endpoint that uses external API calls (consider using a mock for the external service).
By practicing these exercises, you'll strengthen your skills in writing comprehensive integration tests for FastAPI applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)