Table of contents
Overview
For the series “startup project with…” I’m going to approach python web development and I’ve tried to use FastAPI web framework. I was very surprised by it’s easy of use and I wanted to go in deeper about it; above all it’s testing features.
Pet project
In order to test functionality I implemented this pet project. Basically it is a simple server with one rest api.
Local Run
After checkout the code you can simply execute “script/ci” on your terminal application. The script will execute these steps:
- build python app in docker container
- code checks (like unused code)
- test execution
If you want to try the API in a local host container you can just execute these commands:
docker-compose up api
curl http://localhost:8080/items/123\?q\="test"
The second one should provide this response:
{"item_id":123,"q":"test","use_case":"Production Code"}
Scope
Regarding the code design I just wanted to achieve two points:
- encapsulate controller api on a class
- inject collaborator in order to easily test API
Regarding the point 1) I have:
class AppController:
def __init__(self, app: FastAPI, use_case: UseCase):
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
return {
"item_id": item_id, "q": q, "use_case": use_case.run()
The class AppController could be built with a FastAPI and a UseCase objects. The first one is something related FastAPI configuration and run the second one is my business logic.
Having this class I can configure my application using a startup() method where wiring the code I need for the App:
def startup(use_case: UseCase = ProductionUseCase()):
app = FastAPI()
AppController(app, use_case)
return app
Finally I can run my application using docker container. This is the container definition:
FROM python:3.10.3-slim-bullseye
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
CMD ["uvicorn", "app.main:startup", "--host", "0.0.0.0", "--port", "8080", "--workers", "4"]
Testing
Off course I can also use startup() method for injecting dependencies in order to easily test the application. These are two tests for the controller:
def test_e2e_production_code(self):
client = TestClient(startup())
response = client.get("/items/987?q=this%20is%20the%20query")
self.assertEqual(200, response.status_code)
self.assertEqual(
{"item_id": 987, "q": "this is the query", "use_case": "Production Code"},
response.json()
)
class TestableUseCase(UseCase):
def run(self):
return "Test Code"
def test_e2e_testable_code(self):
client = TestClient(startup(TestableUseCase()))
response = client.get("/items/987?q=this%20is%20the%20query")
self.assertEqual(200, response.status_code)
self.assertEqual(
{"item_id": 987, "q": "this is the query", "use_case": "Test Code"},
response.json()
)
The first test is using production code dependencies, building the client with production use case:
client = TestClient(startup())
Sometimes, during the development, we cannot use real connection or we just want to decompose test (unit, integration …) using stubbed collaborators (ex. contract test). For this reason we could inject a TestableCollaborator like in the second test (test_e2e_testable_code):
client = TestClient(startup(TestableUseCase()))
Considerations
From design point of view FastAPI is super useful framework to use to build our microservices. Fits very well with container logic and cloud development. It’s also very easy to build a test suite for the application we’re going to develop. Of course you have to pay attention to not introduce complexity with some feature it provides like dependencies or DBMS access.