Use TestClient to verify HTTP behavior without starting a real server
Day 69 of 80
FastAPI is built on Starlette, which provides a TestClient class. It lets you make HTTP requests directly to your FastAPI app in memory — no uvicorn, no port, no subprocess. You import your app, wrap it in TestClient, and call client.get() / client.post() as if you were making real requests.
The responses are real HTTP response objects with .status_code, .json(), and all the standard fields.
TestClient comes with FastAPI via Starlette. You do need httpx installed for it to work — run pip install httpx if you haven't already. FastAPI's docs mention this as a dependency for testing.
from fastapi.testclient import TestClient
from api import app
client = TestClient(app)
def test_list_prompts():
response = client.get("/prompts")
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_prompt():
data = {
"platform": "Kling",
"shot": "Test shot",
"prompt_text": "Cinematic test"
}
response = client.post("/prompts", json=data)
assert response.status_code == 201
result = response.json()
assert result["platform"] == "Kling"
def test_create_invalid_platform():
data = {
"platform": "Pika",
"shot": "test",
"prompt_text": "test"
}
response = client.post("/prompts", json=data)
assert response.status_code == 422
def test_get_stats():
response = client.get("/stats")
assert response.status_code == 200
assert isinstance(response.json(), dict)
| Code | Meaning | When You See It |
|---|---|---|
| 200 OK | Request succeeded, data returned | GET requests that find data |
| 201 Created | Resource was created | Successful POST to create a prompt |
| 404 Not Found | Resource doesn't exist | GET /prompts/999 when 999 doesn't exist |
| 422 Unprocessable Entity | Validation failed | POST with invalid platform or missing field |
| 500 Internal Server Error | Unhandled exception in your code | A bug you need to fix — never expected in tests |
Both work. The convention is: use 201 Created when a resource is successfully created, 200 OK when you're returning data from an existing resource. FastAPI lets you set this with @app.post("/prompts", status_code=201).
The most basic test: call GET /prompts, get 200, get back a list. It doesn't care what's in the list — just that the endpoint exists, returns success, and returns the right data type.
POST a valid prompt, expect 201, and verify the response includes the platform we sent. This tests the full creation pipeline: request body parsing, validation, model creation, response serialization.
"Pika" is not in your valid platforms list. Your API should reject it and return 422. This test verifies that your validation logic is wired into the API correctly — it's not enough for validation to exist in the model if the API doesn't surface the error properly.
The stats endpoint returns a summary dictionary. This test only verifies that the endpoint works and returns a dict — not the specific counts. That's appropriate: counts depend on how many prompts exist, which varies between runs.
Sometimes you want to look at the full response during debugging. Add print statements temporarily:
def test_create_prompt():
data = {"platform": "Kling", "shot": "Test shot", "prompt_text": "Cinematic test"}
response = client.post("/prompts", json=data)
# Temporarily print to inspect structure (remove before committing)
print("\nStatus:", response.status_code)
print("Response:", response.json())
assert response.status_code == 201
pytest tests/test_api.py::test_create_prompt -v -s — the -s flag lets print statements through. This is standard debugging technique for API tests.Extend test_api.py with a test for a missing resource:
def test_get_nonexistent_prompt():
response = client.get("/prompts/99999")
assert response.status_code == 404
def test_delete_prompt():
# First create a prompt to delete
data = {"platform": "Veo", "shot": "Delete test", "prompt_text": "Will be deleted"}
create_response = client.post("/prompts", json=data)
assert create_response.status_code == 201
# Then delete it
prompt_id = create_response.json()["id"]
delete_response = client.delete(f"/prompts/{prompt_id}")
assert delete_response.status_code == 200
$ pytest tests/ -v
tests/test_models.py::TestPrompt::test_create_valid_prompt PASSED
tests/test_models.py::TestPrompt::test_platform_cleaned PASSED
tests/test_models.py::TestPrompt::test_invalid_platform_raises_error PASSED
tests/test_models.py::TestPrompt::test_empty_shot_raises_error PASSED
tests/test_models.py::TestPrompt::test_str_representation PASSED
tests/test_models.py::TestPrompt::test_preview_long PASSED
tests/test_models.py::TestPrompt::test_to_dict_roundtrip PASSED
tests/test_models.py::TestPromptLibrary::test_empty_library PASSED
tests/test_models.py::TestPromptLibrary::test_add_and_retrieve PASSED
tests/test_models.py::TestPromptLibrary::test_persistence PASSED
tests/test_models.py::TestPromptLibrary::test_search PASSED
tests/test_models.py::TestPromptLibrary::test_delete PASSED
tests/test_api.py::test_list_prompts PASSED
tests/test_api.py::test_create_prompt PASSED
tests/test_api.py::test_create_invalid_platform PASSED
tests/test_api.py::test_get_stats PASSED
=================== 16 passed in 0.45s ===================
pytest tests/ and confirm everything still passes.tests/test_api.py using FastAPI's TestClientpytest -s to print response data during debuggingpytest tests/ -v) shows all tests passingDay 70 is the Week 14 review. You'll consolidate everything learned about pytest — test structure, assertions, raises, tmp_path fixtures, and TestClient — and get a preview of Week 15's CLI tools with Typer.