Skip to content

Provider Registration and Testing Dependencies

We've already seen one way of telling the dependency injection system how to wire a dependency that it can't auto-wire in the form of Markers.

There are however other situations where Markers may not be the answer. For these situations, Xpresso offers provider registration which lets you dynamically bind a provider to a dependency. When you register a provider, you completely replace the original provider (if any) in Xpresso's directed acyclic graph of dependencies. This means that any sub-dependencies of the original provider (if any) will not be executed. This also means that the provider you are registering can itself have sub-dependencies. Those will get treated just like any other dependency, all of the same rules apply.

As an example, let's look at how we might write a test for our ongoing httpx.AsyncClient examples. Here is the example we had previously from the Dependency Injection - Introduction section:

import httpx

from xpresso import App, Path


async def echo_url(client: httpx.AsyncClient) -> str:
    resp = await client.get("https://httpbin.org/get")
    resp.raise_for_status()  # or some other error handling
    return resp.json()["url"]


app = App(
    routes=[
        Path(
            "/echo/url",
            get=echo_url,
        )
    ]
)

We don't want to actually make network calls to HTTPBin in our tests, so we swap out the httpx.AsyncClient for one using httpx.MockTransport:

import httpx

from docs_src.tutorial.dependencies.tutorial_001 import app
from xpresso import Dependant
from xpresso.testclient import TestClient


def test_client_injection():
    async def handler(request: httpx.Request) -> httpx.Response:
        assert request.url == "https://httpbin.org/get"
        return httpx.Response(200, json={"url": "https://httpbin.org/get"})

    with app.container.register_by_type(
        Dependant(lambda: httpx.AsyncClient(transport=httpx.MockTransport(handler))),
        httpx.AsyncClient,
    ):
        client = TestClient(app)
        response = client.get("/echo/url")
        assert response.status_code == 200, response.content
        assert response.json() == "https://httpbin.org/get"

Tip

You can use container.register_by_type both as a context manager (like in the example above) and as a plain function call. If used as a plain function call, the binding of the provider to the dependency is permanent. If used as a context manager, the binding will be reversed when the context manager exits. You should use the context manager form in tests so that you don't leak state from one test to another.

Tip

We use a lambda here to tell the dependency injection system what parameters to include. But remember that this lambda will be called every time the dependency is requested. You can also create an instance outside of the lamdbda function and return that if you want to use the same instance everywhere. In this particular test, it doesn't make a difference.

You can also use this same mechanism to declare a dependency on an abstract interface (including typing.Protocol classes) and then register a concrete provider in some create_production_app() class.