Dave
Dave

Reputation: 8091

How to create unit tests for a FastAPI endpoint that makes request to another endpoint?

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

Answers (1)

Gino Mempin
Gino Mempin

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

Related Questions