3 The distance endpoint, and integration tests
Read the tutorial. Build it yourself is the other way to read this part.
This walk goes through rahuldave/cheesecracker on GitHub. This part starts from the code at step-02. The code before this part
To look at this code on your machine, run these commands. The first one makes a clone, if this folder has none:
[ -d cheesecracker ] || git clone https://github.com/rahuldave/cheesecracker
cd cheesecracker
git fetch --quiet --tags --force origin
git switch --detach step-02The server gets a new request, /euclidean_distance/, which works out how far a point is from the origin. New tests
send that request to the app, and check the answer.
just setupThe app needs an endpoint that does some work, and tests that call it. The endpoint computes the distance of a point
from the origin with power(). An integration test sends requests to the app and checks the answers, with no server
and no network.
3.1 /euclidean_distance/, the distance of a point from the origin
The Cheese App asks the server for the distance of a point (x, y) from the origin (0, 0). That distance is the
square root of x squared plus y squared.
± See the change to src/api-service/api/service.py
service.py now imports math, and power() from api.utils. The new function at the end,
calculate_euclidean_distance(), answers /euclidean_distance/. It takes x and y, with 1 and 2 as defaults,
and returns them with the distance.
Start the server, and ask it for the distance of the point (3, 4). The server runs to the end of this part, and
restarts when a file of the code changes, because of --reload.
just serve
curl -s "localhost:9000/euclidean_distance/?x=3&y=4"The server answers {"x":3.0,"y":4.0,"distance":5.0}.
What changed
src/api-service/api/service.py: the imports ofmathandpower(), and the endpoint/euclidean_distance/. 12 lines added. The file- look at
z = power(x, 2) + power(y, 2)
- look at
The diff of src/api-service/api/service.py
@@ -4,6 +4,8 @@ Cheese App API Service
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
+import math
+from api.utils import power
# Setup FastAPI app
app = FastAPI(title="Cheese API Server", description="API Server for Cheese App", version="v1")
@@ -27,3 +29,13 @@ async def get_index():
@app.get("/health")
async def health_check():
return {"status": "healthy"}
+
+
+@app.get("/euclidean_distance/")
+async def calculate_euclidean_distance(x: float = 1, y: float = 2):
+ """Calculate the Euclidean distance from origin: sqrt(x^2 + y^2)
+ Returns the distance of a point (x, y) from the origin (0, 0)
+ """
+ z = power(x, 2) + power(y, 2)
+ result = math.sqrt(z)
+ return {"x": x, "y": y, "distance": result}3.2 httpx2 and pytest-asyncio, for tests of the app
FastAPI's TestClient sends requests to the app in the same
process, with no server. It needs httpx2, a library that sends HTTP requests.
One test of the app is a coroutine, an async function. pytest cannot run an async function by itself, so
pytest-asyncio runs it.
This change came from running uv add --group test httpx2 pytest-asyncio.
The command adds both packages to the group test, and writes their versions into uv.lock.
just test -qpytest still prints 11 passed. The new packages change no test.
What changed
pyproject.toml: httpx2 and pytest-asyncio, in the grouptest. 2 lines added. It comes fromuv add --group test httpx2 pytest-asyncio. The file- look at
"httpx2>=2.13.1",
- look at
uv.lock: the exact versions of httpx2, pytest-asyncio, and the packages that they need. 68 lines added, 1 removed. It comes fromuv add --group test httpx2 pytest-asyncio. The file
The diff of pyproject.toml
@@ -17,7 +17,9 @@ default-groups = "all"
[dependency-groups]
test = [
+ "httpx2>=2.13.1",
"pytest>=9.1.1",
+ "pytest-asyncio>=1.4.0",
]
[tool.pytest.ini_options]3.3 integration tests of the API
A unit test of power() cannot tell whether the app routes a request to the right function, or reads its
parameters, or answers in JSON. Ten integration tests send requests to the app through TestClient, and check each
answer.
± See the new file tests/integration/test_api.py
The new file tests/integration/test_api.py makes client = TestClient(app). The class TestAPIEndpoints checks
the welcome, the distance of (3, 4) and the defaults. It also checks a route that does not exist (404), and a method
that is not allowed (405). The class TestCORS checks the CORS headers.
just test -qpytest prints 21 passed, the eleven unit tests and the ten integration tests. The tests needed no server, so we
stop the one of just serve.
just stopWhat changed
tests/integration/test_api.py: ten integration tests, through FastAPI'sTestClient. New, 89 lines. The file- look at
response = client.get("/euclidean_distance/?x=3&y=4")
- look at
The diff of tests/integration/test_api.py
@@ -0,0 +1,89 @@
+"""
+Integration tests for the Cheese App API
+Tests the full API endpoints with FastAPI TestClient
+Based on L18, Slide 11-12 - Integration tests
+"""
+
+import pytest
+from fastapi.testclient import TestClient
+from api.service import app
+
+client = TestClient(app)
+
+
+class TestAPIEndpoints:
+ """Integration tests for API endpoints"""
+
+ def test_root_endpoint(self):
+ """Test the root endpoint returns welcome message"""
+ response = client.get("/")
+ assert response.status_code == 200
+ data = response.json()
+ assert "message" in data
+ assert "Cheese App" in data["message"]
+
+ def test_root_returns_json(self):
+ """Test that root returns JSON content type"""
+ response = client.get("/")
+ assert response.status_code == 200
+ assert "application/json" in response.headers["content-type"]
+
+ def test_euclidean_distance_endpoint(self):
+ """Test the euclidean_distance endpoint"""
+ response = client.get("/euclidean_distance/?x=3&y=4")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["x"] == 3
+ assert data["y"] == 4
+ assert data["distance"] == 5.0
+
+ def test_euclidean_distance_default_parameters(self):
+ """Test euclidean_distance with default parameters"""
+ response = client.get("/euclidean_distance/")
+ assert response.status_code == 200
+ data = response.json()
+ assert "distance" in data
+
+ def test_euclidean_distance_with_floats(self):
+ """Test euclidean_distance with floating point numbers"""
+ response = client.get("/euclidean_distance/?x=1.5&y=2.0")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["x"] == 1.5
+ assert data["y"] == 2.0
+ assert isinstance(data["distance"], float)
+
+ def test_invalid_route_returns_404(self):
+ """Test that invalid routes return 404"""
+ response = client.get("/this-route-does-not-exist")
+ assert response.status_code == 404
+
+ def test_method_not_allowed(self):
+ """Test that POST to GET-only endpoint returns 405"""
+ response = client.post("/")
+ assert response.status_code == 405
+
+
+class TestCORS:
+ """Tests for CORS configuration"""
+
+ def test_cors_enabled(self):
+ """Test that CORS headers are present"""
+ response = client.get("/", headers={"Origin": "http://localhost:3000"})
+ assert response.status_code == 200
+ assert "access-control-allow-origin" in response.headers
+
+ def test_cors_allows_all_origins(self):
+ """Test that CORS allows all origins"""
+ response = client.get("/", headers={"Origin": "http://example.com"})
+ assert response.status_code == 200
+ assert response.headers["access-control-allow-origin"] == "*"
+
+
+@pytest.mark.asyncio
+async def test_api_health_check():
+ """Test that the API is healthy and responding"""
+ response = client.get("/")
+ assert response.status_code == 200
+ # API should respond quickly
+ assert response.elapsed.total_seconds() < 1 if hasattr(response, "elapsed") else TrueThis part ends at the code at step-03. The code after this part