How to mock with respx when httpx client is wrapped in a class

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!