I’ve not used Python seriously in a while. While doing a side project I decided to do it in Python since a lot has changed in Python land in the last 3 years and I wanted to re-sharpen my claws…
For my project, I had to consume a REST API. I decided to make a client like the following:
import httpx
from .config import Config
class APIClient:
def __init__(self, config: Config) -> None:
self.config = config
def request(self, method: str, endpoint: str, **kwargs):
url = f"{self.config.base_url}{endpoint}"
with httpx.Client() as c:
response = c.request(method, url, headers=self.config.headers, **kwargs)
response.raise_for_status()
return response
This client worked fine when consuming the REST API. I had DTOs for every request and response payload with Pydantic. Life was beautiful.
Then I had to consume an endpoint that could modify data. So, I decided to use respx
mocks. In my test file, I wrote something like the following:
import respx
from api_pkg import APIClient
base_url = "https://example.com"
mock_data = "..."
data_id = 1
def test_data_get():
with respx.mock(base_url=base_url) as mock:
mock.get(f"/api/v1/{data_id}").respond(200, json=mock_data)
api = APIClient(base_url, api_token)
data = api.datastore.get_data(data_id)
But to my surprise, respx
kept throwing the following error:
E respx.models.AllMockedAssertionError: RESPX: <Request(b'GET', 'https://example.com/api/v1/1')> not mocked!
After a lot of hair pulling I found out that httpx==0.28.0
isn’t compatible with respx
and had to add a “using=httpx” in the context manager. Basically write this: with respx.mock(base_url=base_url, using="httpx") as mock:
according to the github comment. I thought this would solve my issues.
But no luck!
Another round of hairpulling and learning how respx
works, I figured out that since the httpx.Client
is encapsulated another class, respx
can’t monkey patch the client to make the mock work. The client has to be created inside the respx.mock
context. The easiest way I found to solve it was to create the client inside the context and pass it around.
import httpx
import respx
from api_pkg import APIClient
base_url = "https://example.com"
mock_data = "..."
data_id = 1
def test_data_get():
with respx.mock(base_url=base_url) as mock:
mock.get(f"/api/v1/{data_id}").respond(200, json=mock_data)
c = httpx.Client()
api = APIClient(base_url, api_token, c)
data = api.datastore.get_data(data_id)
and the client.py
file had to change as well:
import httpx
from .config import Config
class APIClient:
def __init__(self, config: Config, client: httpx.Client = None) -> None:
self.config = config
self._client = client
def request(self, method: str, endpoint: str, **kwargs):
url = f"{self.config.base_url}{endpoint}"
if self._client is None:
self._client = httpx.Client()
with self._client as c:
response = c.request(method, url, headers=self.config.headers, **kwargs)
response.raise_for_status()
return response
This solved the issue. Now the mocks worked but I’m not sure if this is the best way to do this. If you know any better way of handling cases like these, please let me know!
Thanks!