3 The distance endpoint, and integration tests

Build it yourself. Read the tutorial 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

If you start at this part, make your clone as Get the project says, and then run this command. It puts in your clone the files that this part starts from. A file that the project deleted before this part stays, so delete it if git check lists it:

git fetch --quiet --tags --force origin; git rm -rq --cached --ignore-unmatch -- :/ && git restore --source step-02 --staged --worktree -- :/

The 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 setup

The 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.

Apply the patch to src/api-service/api/service.py, from the top of the project:

curl -fsSL https://github.com/rahuldave/cheesecracker/commit/e576e7bb9e550b1a572a764b15f8d0bfde229d97.diff | git apply --include=src/api-service/api/service.py

Read the two imports after CORSMiddleware, and the new function at the end of the file.

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}.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check e576e7bb9e55

If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:

git fetch --quiet --tags --force origin; git restore --source step-02 --staged -- :/ && git restore --source e576e7bb9e55 --staged --worktree -- :/

What changed

  • src/api-service/api/service.py: the imports of math and power(), and the endpoint /euclidean_distance/. 12 lines added. The file
    • look at z = power(x, 2) + power(y, 2)

The patch to 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}

This change on GitHub The code after it

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.

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 -q

pytest still prints 11 passed. The new packages change no test.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check 3c75ffb1a089

If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:

git restore --source e576e7bb9e55 --staged -- :/ && git restore --source 3c75ffb1a089 --staged --worktree -- :/

What changed

  • pyproject.toml: httpx2 and pytest-asyncio, in the group test. 2 lines added. It comes from uv add --group test httpx2 pytest-asyncio. The file
    • look at "httpx2>=2.13.1",
  • uv.lock: the exact versions of httpx2, pytest-asyncio, and the packages that they need. 68 lines added, 1 removed. It comes from uv add --group test httpx2 pytest-asyncio. The file

The diff that uv add --group test httpx2 pytest-asyncio makes in 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]

This change on GitHub The code after it

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.

Apply the patch to tests/integration/test_api.py, from the top of the project:

curl -fsSL https://github.com/rahuldave/cheesecracker/commit/5862780b44d2f4ec87088c971dec57f23c11139e.diff | git apply --include=tests/integration/test_api.py

Make the folder tests/integration/, and apply the patch. Read the line client = TestClient(app), then the classes TestAPIEndpoints and TestCORS.

just test -q

pytest 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 stop

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check 5862780b44d2

If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:

git fetch --quiet --tags --force origin; git restore --source 3c75ffb1a089 --staged -- :/ && git restore --source step-03 --staged --worktree -- :/

What changed

  • tests/integration/test_api.py: ten integration tests, through FastAPI's TestClient. New, 89 lines. The file
    • look at response = client.get("/euclidean_distance/?x=3&y=4")

The patch to 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 True

This change on GitHub

This part ends at the code at step-03. The code after this part