Disclaimer: I’m learning Python, so please correct me if I’ve written something wrong!
In this tutorial, we’ll explore how to test HTTP client calls in a FastAPI Python application using two different methods:
-
unittest.mock
: This is a built-in Python library used for mocking objects in tests. It allows you to replace parts of your system under test and make assertions about how they have been used. -
httpretty
: This is a third-party library that allows you to mock HTTP requests at a low level by creating a fake HTTP server.
To run the tests, just execute this on the root of the project directory:
pytest
You should see output similar to this:
$ pytest
============================================================================================== test session starts ==============================================================================================
platform linux -- Python 3.11.6, pytest-8.2.0, pluggy-1.5.0
rootdir: /home/wsl/github/python-study/api-client
plugins: anyio-4.2.0
collected 3 items
tests/test_main.py . [ 33%]
tests/test_main_httppretty_mock.py . [ 66%]
tests/test_main_magic_mock.py . [100%]
=============================================================================================== 3 passed in 0.39s ===============================================================================================
This output indicates that all three tests passed successfully. The percentage in brackets shows the progress of the test run.
Versions of my modules
$ pip list
Package Version
---------------------------------- -----------
fastapi 0.111.0
httpretty 1.1.4
pydantic 2.5.3
pytest 8.2.0
requests 2.31.0
Project structure
.
├── app
│ ├── __init__.py
│ └── main.py
└── tests
├── __init__.py
├── test_main_httppretty_mock.py
└── test_main_magic_mock.py
__init__.py
are just empty files
main.py
from pydantic import BaseModel
from fastapi import FastAPI, HTTPException
from typing import Union
import requests
import os
import logging
API_SERVER_URL = os.getenv("API_SERVER_URL", default="http://localhost:8010")
API_LOG_LEVEL = os.getenv("API_LOG_LEVEL", default="INFO")
# Configure basic logging
logging.basicConfig(level=API_LOG_LEVEL)
logger = logging.getLogger()
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: Union[bool, None] = None
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: int, q: Union[str, None] = None):
try:
response = requests.get(
f"{API_SERVER_URL}/items/{item_id}", params={"q": q})
# Raises HTTPError for bad responses (4XX or 5XX)
response.raise_for_status()
item_data = response.json()
item = Item(**item_data) # Deserialize the JSON into an Item object
return item
except requests.RequestException as e:
logger.error(f"Request failed: {str(e)}") # Log the error
raise HTTPException(status_code=500, detail=str(e))
test_main_magic_mock.py
from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch, MagicMock
from app.main import app
client = TestClient(app)
@pytest.fixture
def mock_requests_get():
# Create a MagicMock object to mock requests.get
with patch('requests.get') as mock_get:
# Prepare a mock response object with necessary methods
mock_response = MagicMock()
mock_response.json.return_value = {
"name": "Sample Item",
"price": 100.0,
"is_offer": None
}
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
yield mock_get
# The mock_requests_get fixture is automatically used here.
def test_read_item(mock_requests_get):
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {
"name": "Sample Item",
"price": 100.0,
"is_offer": None
}
# Assert if requests.get was called correctly
mock_requests_get.assert_called_once_with(
"http://localhost:8010/items/1",
params={'q': None}
)
test_main_httppretty_mock.py
import httpretty
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
@httpretty.activate
def test_read_item():
# Mocking the external API call
httpretty.register_uri(
httpretty.GET,
"http://localhost:8010/items/1",
body='{"name": "Sample Item", "price": 100.0, "is_offer": null}',
content_type="application/json",
status=200 # Define the expected status code
)
# Call the endpoint
response = client.get("/items/1")
# Assertions
assert response.status_code == 200
assert response.json() == {
"name": "Sample Item",
"price": 100.0,
"is_offer": None
}
Top comments (0)