Reputation: 8091
I have a FastAPI app that makes requests to other endpoint within a function that handles a particular request.
How can I build unit tests for this endpoint using fastapi.testclient.TestClient
?
import fastapi
import requests
import os
app = fastapi.FastAPI()
# in production this will be a k8s service to allow for
# automatic scaling and load balancing.
# The default value works when the app is run using uvicorn
service_host = os.environ.get('SERVICE_HOST', 'localhost:8000')
@app.post('/spawner')
def spawn():
# an endpoint that makes multiple requests to set of worker endpoints
for i in range(4):
url = 'http://'+service_host+f'/create/{i}'
requests.post(url)
return {'msg': 'spawned the creation/update of 4 resources'}
@app.post('/create/{index}')
def worker(index: int):
# an endpoint that does the actual work of creating/updating the resource
return {'msg': f'Created/updated resource {index}'}
Upvotes: 4
Views: 6939
Reputation: 29536
How to write the unit tests depends on what specifically you want to be checking.
For example, from your sample code, if you just want to check that your /spawner
endpoint properly calls your /create
endpoint a certain number of times, you can use Python's unittest.mock.patch
to patch the requests.post
call such that it doesn't make the actual call, but you can inspect the call it would have made. (See How can I mock requests and the response? for examples on how to mock external calls made by the requests
library.)
You still use FastAPI's TestClient
to make the call to your endpoint, but while the .post
call is patched:
from fastapi.testclient import TestClient
from main import app
from unittest.mock import patch
def test_spawn():
client = TestClient(app)
mocked_response = requests.Response()
mocked_response.status_code = 200
with patch("requests.post", return_value=mocked_response) as mocked_request:
response = client.post("/spawner")
# Expect that the requests.post was called 4 times
# and it called the /create/{index} endpoint
assert mocked_request.call_count == 4
called_urls = [call.args[0] for call in mocked_request.call_args_list]
assert called_urls[0].endswith("/create/0")
assert called_urls[1].endswith("/create/1")
assert called_urls[2].endswith("/create/2")
assert called_urls[3].endswith("/create/3")
# Expect to get the msg
assert response.status_code == 200
assert response.json() == {"msg": "spawned the creation/update of 4 resources"}
patch
returns a Mock
object, which has attributes like call_count
telling you how many times the requests.post
function was called and a call_args_list
storing the arguments for each call to requests.post
. Then, your test can also assert that if everything behaved as expected, then it should return the expected response.
Now, going back to what I said, the usefulness of this test largely depends on what specifically are you hoping to gain from writing the tests. One problem with this test is that it isn't strictly black-box testing, because the test "knows" how the endpoint is supposed to work internally. If the point is to make sure, for example, that calling /spawner
with some query parameter ?count=N
results in N
number of spawned resources, then maybe this kind of test is useful.
Another thing, this kind of test makes sense if you are making actual external calls to some other API, and not to your own endpoint. User JarroVGit mentioned in the comments that a better implementation would be, instead of making a POST request, you can refactor /spawner
to call the endpoint function directly and pass the /create
response:
@app.post("/spawner")
def spawn():
# an endpoint that makes multiple requests to set of worker endpoints
results = [worker(i) for i in range(4)]
return {
"msg": "spawned the creation/update of 4 resources",
"results": results,
}
@app.post("/create/{index}")
def worker(index: int):
# an endpoint that does the actual work of creating/updating the resource
worker_inst.create(index)
return {"msg": f"Created/updated resource {index}"}
The test can be made simpler by patching whatever is in /create
to not create the actual resources (here, I'm assuming some sort of a worker_inst.create(index)
that does the actual creation):
def test_spawn():
client = TestClient(app)
# Patch the actual create/update resources to *not* create anything
with patch.object(WorkerInst, "create"):
response = client.post("/spawner")
# Expect to get the msg
assert response.status_code == 200
assert response.json() == {
"msg": "spawned the creation/update of 4 resources",
"results": [
{"msg": "Created/updated resource 0"},
{"msg": "Created/updated resource 1"},
{"msg": "Created/updated resource 2"},
{"msg": "Created/updated resource 3"},
],
}
I don't know what kind of "resources" your app is creating, but you can possibly convert that unit test into a more useful integration test, by removing the patch
-ing of worker
and just letting the worker create the resources, then let the test assert that 1) the correct resources were created, and 2) the /spawner
endpoint returned the expected response. Again, depends on what "resources" is and the purpose of your tests.
Upvotes: 3