FastAPI Extended Testing with unittest module
Table of Contents
Preface
In this article, I will demonstrate how to reliably extend the
functionality of the existing code using the power and versatility of
OpenAI
. We will explore practical approaches that ensure a robust
enhancement process, allowing developers to build upon tested
solutions with confidence. Throughout the article, I will provide
examples and best practices to guide you in implementing these
enhancements effectively.
The primary objective of this article is to demonstrate how to extend
the functionality of an already working and well-tested piece of
Python
code. This is achieved by utilizing OpenAI
's main feature:
the ability to identify patterns and extrapolate by analogy.
The secondary objective here is to exhibit how to do it in no time and with maximum clarity and precision.
Prerequisites
In the prior article I've explained how to locate the code of
application and its test suite in a single Babel
code-snippet. It
allows to read them smoothly altogever, and, nevertheless to launch
the test suits and get results as well.
In the previous article, I explained how to locate both the
application code and its test suite within a single Babel
code
snippet. This approach allows for seamless reading of both components
while also enabling the execution of the test suite and obtaining
results.
Concocting The Query
It may sound trivial, but all you need to do is clearly articulate what you really want in relation to what you currently have.
Explain What You Already Have
I've wrapped the app code and the test suite into a single Babel code snippet:
import unittest from typing import Union from fastapi import FastAPI from fastapi.testclient import TestClient app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} # # # The Test Suite # client = TestClient(app) class TestFastAPI(unittest.TestCase): def test_read_root(self): response = client.get("/") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"Hello": "World"}) def test_read_item_with_query(self): response = client.get("/items/42?q=somequery") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 42, "q": "somequery"}) def test_read_item_without_query(self): response = client.get("/items/42") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 42, "q": None}) unittest.main(exit=False)
... ---------------------------------------------------------------------- Ran 3 tests in 0.026s OK
and it works.
Explain What Do You Want
Now I want to:
extend the basic FastAPI application to use all HTTP request methods (GET, PUT, POST, DELETE, PATCH, HEAD, OPTIONS, TRACE); extend the test suite in accordion with these methods.
Retrieving The Answer
Certainly! Let's extend the FastAPI application to support all the
HTTP request methods (GET
, POST
, PUT
, DELETE
, PATCH
, HEAD
,
OPTIONS
, TRACE
), and also extend the test suite to test these
endpoints.
Extended FastAPI Application with All HTTP Methods
import unittest from typing import Union from fastapi import FastAPI from fastapi.testclient import TestClient app = FastAPI() # In-memory storage for demonstration purposes items = {} @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} @app.post("/items/") def create_item(item_id: int, item: dict): items[item_id] = item return {"item_id": item_id, "item": item} @app.put("/items/{item_id}") def update_item(item_id: int, item: dict): if item_id not in items: return {"Error": "Item not found"}, 404 items[item_id] = item return {"item_id": item_id, "item": item} @app.delete("/items/{item_id}") def delete_item(item_id: int): if item_id not in items: return {"Error": "Item not found"}, 404 del items[item_id] return {"Message": "Item deleted"} @app.patch("/items/{item_id}") def patch_item(item_id: int, item: dict): if item_id not in items: return {"Error": "Item not found"}, 404 items[item_id].update(item) return {"item_id": item_id, "item": items[item_id]} @app.head("/items/{item_id}") def head_item(item_id: int): if item_id not in items: return {"Error": "Item not found"}, 404 return items[item_id] @app.options("/items/{item_id}") def options_item(item_id: int): return {"Allow": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE"]} @app.trace("/items/") def trace_items(): return {"Message": "TRACE endpoint"} # # # The Test Suite # client = TestClient(app) class TestFastAPI(unittest.TestCase): def test_read_root(self): response = client.get("/") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"Hello": "World"}) def test_read_item_with_query(self): response = client.get("/items/42?q=somequery") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 42, "q": "somequery"}) def test_read_item_without_query(self): response = client.get("/items/42") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 42, "q": None}) def test_create_item(self): response = client.post("/items/", json={"item_id": 1, "item": {"name": "Item1"}}) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 1, "item": {"name": "Item1"}}) def test_update_item(self): client.put("/items/", json={"item_id": 1, "item": {"name": "Item1"}}) response = client.put("/items/1", json={"name": "Updated Item1"}) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 1, "item": {"name": "Updated Item1"}}) def test_delete_item(self): client.post("/items/", json={"item_id": 1, "item": {"name": "Item1"}}) response = client.delete("/items/1") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"Message": "Item deleted"}) def test_patch_item(self): client.post("/items/", json={"item_id": 1, "item": {"name": "Item1"}}) response = client.patch("/items/1", json={"description": "Updated description"}) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"item_id": 1, "item": {"name": "Item1", "description": "Updated description"}}) def test_head_item(self): client.post("/items/", json={"item_id": 1, "item": {"name": "Item1"}}) response = client.head("/items/1") self.assertEqual(response.status_code, 200) def test_options_item(self): response = client.options("/items/1") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"Allow": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE"]}) def test_trace_items(self): response = client.request("TRACE", "/items/") self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"Message": "TRACE endpoint"}) unittest.main(exit=False)
FF..F....F ====================================================================== FAIL: test_create_item (__main__.TestFastAPI.test_create_item) ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/babel-C8lSuP/python-7NQNHi", line 84, in test_create_item self.assertEqual(response.status_code, 200) AssertionError: 422 != 200 ====================================================================== FAIL: test_delete_item (__main__.TestFastAPI.test_delete_item) ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/babel-C8lSuP/python-7NQNHi", line 97, in test_delete_item self.assertDictEqual(response.json(), {"Message": "Item deleted"}) AssertionError: [{'Error': 'Item not found'}, 404] is not an instance of <class 'dict'> : First argument is not a dictionary ====================================================================== FAIL: test_patch_item (__main__.TestFastAPI.test_patch_item) ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/babel-C8lSuP/python-7NQNHi", line 103, in test_patch_item self.assertDictEqual(response.json(), {"item_id": 1, "item": {"name": "Item1", "description": "Updated description"}}) AssertionError: [{'Error': 'Item not found'}, 404] is not an instance of <class 'dict'> : First argument is not a dictionary ====================================================================== FAIL: test_update_item (__main__.TestFastAPI.test_update_item) ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/babel-C8lSuP/python-7NQNHi", line 91, in test_update_item self.assertDictEqual(response.json(), {"item_id": 1, "item": {"name": "Updated Item1"}}) AssertionError: [{'Error': 'Item not found'}, 404] is not an instance of <class 'dict'> : First argument is not a dictionary ---------------------------------------------------------------------- Ran 10 tests in 0.037s FAILED (failures=4)
Explanation of the Extended Application
GET /items/{item_id}
: Retrieves an item with optional query parameters.POST /items/
: Creates a new item.PUT /items/{item_id}
: Updates an existing item.DELETE /items/{item_id}
: Deletes an item.PATCH /items/{item_id}
: Partially updates an item.HEAD /items/{item_id}
: Retrieves metadata for an item.OPTIONS /items/{item_id}
: Lists allowed methods.TRACE /items/
: Echoes the received request, used for diagnostics.
Explanation of the Extended Test Suite
- Test for Each HTTP Method: Each test validates the respective HTTP method:
test_read_root
: Tests the rootGET /
endpoint.test_read_item_with_query
andtest_read_item_without_query
: Test theGET /items/{item_id}
endpoint with and without query parameters.test_create_item
: Tests thePOST /items/
endpoint.test_update_item
: Tests thePUT /items/{item_id}
endpoint.test_delete_item
: Tests theDELETE /items/{item_id}
endpoint.test_patch_item
: Tests thePATCH /items/{item_id}
endpoint.test_head_item
: Tests theHEAD /items/{item_id}
endpoint.test_options_item
: Tests theOPTIONS /items/{item_id}
endpoint.test_trace_items
: Tests theTRACE /items/
endpoint.
Conclusion
This setup ensures your FastAPI application correctly handles all major HTTP request methods and that the corresponding endpoints function as expected. You can simply run this code block in your Org-mode file, and it will execute the tests for the extended FastAPI application.