How do mocked tests actually test an API response if the actual HTTP call is not made?
They don't.
... is mock tests only suggested only when an API call itself is not tested (i.e. testing some other feature that has an API call)?
In short, YES.
It is especially used in unit tests for testing specific portions of the code.
If your goal is to
...test if the API is accessible, that the response is correct, concurrent calls limit, any other authorization problems, etc..
Then, mocking your API calls and using mocking libraries is definitely not the solution you need. What you are looking for is somewhere in the area of End-to-End Testing, which verifies how the code/APIs work under real-world conditions, such as from an actual user perspective. In that case, you need to actually call the APIs.
The goal of tests with mocked API calls is done not to test the actual API but to test the behavior/logic of the code surrounding or dependent on the API call, such as how your code handles receiving different kinds of responses from your API or what you do with the response after successfully calling the API. It allows you to write reproducible tests that clearly defines/specifies how your code is expected to behave (related: TDD), without needing to repeatedly call the API or relying on external dependencies (ex. setting up API auth, etc.).
Another advantage of mocking the API calls is that it allows you to repeatedly run your tests even on a local or isolated environment without a network connection or without the proper API access credentials. This can be especially useful for CI pipelines that might be continuously running somewhere and might not have access to your API.
Let's take this code for example:
import json
import requests
from requests import Response
def call_api() -> bool:
res: Response = requests.post(API_URL, json={"quantity": 1})
if res.status_code != 200:
raise Exception(f"API returned an error: {res.text}")
try:
message = res.json()["message"]
except (json.JSONDecodeError, KeyError):
raise Exception("API returned unknown response format")
return message
It calls an API, expects a JSON response with a "message"
key, and returns that "message"
part of that response. It then includes some error-handling for cases when the API did not respond as expected (to simplify, it just raises a bare Exception
).
The easily reproducible way to write tests for those error cases is to mock the response.
import pytest
import requests_mock
@pytest.mark.parametrize("status_code", [401, 403, 500])
def test_call_api_but_returned_not_200(status_code: int):
"""Expect our code to raise an Exception if it's not a 200"""
with requests_mock.Mocker() as mocker:
mocker.post(API_URL, status_code=status_code, text="API Error")
with pytest.raises(Exception) as exc:
call_api()
# Expect that the Exception contains the exact error in the response
assert "API Error" in str(exc.value)
def test_call_api_but_returned_unknown_format():
"""Expect our code to raise an Exception if we can't handle the response format"""
with requests_mock.Mocker() as mocker:
mocker.post(API_URL, status_code=200, json={"unexpected": "keys"})
with pytest.raises(Exception) as exc:
call_api()
assert "API returned unknown response format" in str(exc.value)
In the above tests, you don't need to actually call the API because 1) you might not be able to consistently reproduce these error cases, and 2) you don't really need an exact attribute from the response, just whatever is in the Response
object.
Actually, even for the success cases, it can be useful to mock, just to define that that particular function returns whatever is set in the "message"
attribute of the API response (again, related: TDD).
def test_call_api_and_return_message():
"""Expect our code to return the `message` attr as-is"""
with requests_mock.Mocker() as mocker:
mocker.post(API_URL, status_code=200, json={"message": "YeS, it worked!"})
result = call_api()
assert result == "YeS, it worked!"