Articles

Mocking Dependencies in Litestar

ALittleMoronSoftware Testing0 views
A modular Litestar test application in which a real dependency is replaced with a cyan mock component.

Abstract

When I was building a side project with Litestar, I ran into an annoying problem: I needed to replace a use case with a mock in my API tests, but a regular mocker.patch changed nothing. Litestar did not care about my patch because the dependency had already been assembled through its DI mechanism.

At the time, the working solution was to create a test application with overridden dependencies and pass the mock directly to Provide. The handler would then receive the test object instead of the real use case, and the test could modify that object's state.

Today, I would not treat this as a good general-purpose solution. It is more of a story about the boundaries of framework-level DI. As long as the application is small and has a single entry point, using Provide may be enough. Once you need to mock dependencies consistently across different layers and tests, a separate composition root starts to make more sense.

Context

I wanted to test the API layer as a contract: the handler receives a request, calls the use case, and returns the correct response. I did not want to test the use case itself in the same test. That could be covered separately with unit tests or BDD scenarios, where the business logic actually matters.

In other words, I wanted roughly this level of control in an API test:

  • configure the use-case mock with the data it should return;
  • call the HTTP endpoint;
  • verify the status code and JSON response;
  • avoid hitting the real storage or assembling the entire business flow.

That is a reasonable requirement for a layered application. An API test does not have to become an integration test for the whole service.

Where I Got Stuck

The problem was that the dependency was not being created where I was trying to patch it. The handler received the object through Litestar's DI system, so in my case mocker.patch never reached the actual composition point.

Maybe I was doing something wrong at the time. But the more important point for the test was this: if the framework assembles the dependencies, they often need to be overridden through the framework as well. Otherwise, you get a strange situation where the patch is in place and the test is running, but the handler still receives the real object.

That is how I ended up with a fixture containing the application's dependencies.

A Workaround That Worked

In conftest.py, I kept the use-case mock as a session-scoped fixture and passed it into the test application's dependencies:

@pytest.fixture(scope="session")
def mock_list_items_use_case() -> MockListItems:
    return MockListItems()


@pytest.fixture(scope="session")
def app_dependencies(
    mock_storage: MockStorage,
    mock_list_items_use_case: MockListItems,
) -> Mapping[str, Provide]:
    return {
        "storage": provide_async(mock_storage),
        "list_items_use_case": provide_async(
            mock_list_items_use_case,
        ),
    }


@pytest.fixture(scope="session")
def test_app(app_dependencies: Mapping[str, Provide]) -> Litestar:
    return create_app(debug=True, plugins=get_plugins(), deps=app_dependencies)

The handler continued to receive the dependency as usual, but in the test application that dependency name now pointed to the mock:

ListItemsUseCaseDeps = Annotated[
    ListItemsUseCase,
    Dependency(skip_validation=True),
]


@get("items/")
async def list_items_handler(
    list_items_use_case: ListItemsUseCaseDeps,
) -> ItemsListSchema:
    items = await list_items_use_case.execute()
    return ItemsListSchema.from_domain_schema(schema=items)

It was not the prettiest code in the world, but it solved a specific problem: the API test could control the use case's behavior without using the real storage.

Why skip_validation Came Up

The most important part of that old solution was Dependency(skip_validation=True).

Litestar looks at the declared dependency type and may validate the value passed to the handler. If you provide a completely different object, such as MockListItems, instead of ListItemsUseCase, that validation gets in the way. In my case, I needed to tell the framework that I understood the object was not an instance of that exact class. For this test, I cared about the contract rather than an exact type match.

If the mock implements the same abstract interface, this problem may not appear at all. Another option in this example would have been to mock the storage dependency, keep the real use case, and avoid touching skip_validation. But that would have been a different type of test. At the time, I wanted to verify only the HTTP contract, so I replaced the entire use case.

There is an important caveat regarding current versions of Litestar: this is a historical example. In the current documentation, dependency parameters are explicitly marked with NamedDependency, while validation bypassing is demonstrated with SkipValidation. At the same time, Dependency(skip_validation=True) is still described in the parameter reference. If I were rewriting this code as a tutorial today, I would check the Litestar version and update the example to match its current API.

What Is Unpleasant About This Solution

The main problem is shared state.

The mock lives at session scope, while individual tests modify its state. That means the object has to be reset manually after every test:

@pytest.fixture(autouse=True)
def setup(
    mock_list_items_use_case: MockListItems,
) -> Generator[None, None, None]:
    self.use_case = mock_list_items_use_case
    yield
    self.use_case.items = []

It works, but it requires discipline. Forget to clear the list, and the next test receives data left behind by the previous one. Add another field to the mock, and now you have to remember to reset that as well. The more mocks like this you have, the more your test infrastructure begins to live a life of its own.

The second problem is that the tests become coupled to a specific framework's DI system. Litestar has one way to override dependencies, FastAPI has another, a message broker has a third, and a CLI may require manual assembly. That is tolerable while the project is small. Once the application has several entry points, it starts to feel as if the tests are reproducing the architecture of the frameworks rather than the architecture of the application.

My frustration here is not limited to this particular workaround. Litestar's DI system itself feels overcomplicated: Provide, named dependencies, validation, skip_validation, and separate rules for dependency kwargs. When all you want is to replace a use case in a test, there is too much framework machinery to keep in mind. As far as I remember, Ilya Sobolev, who contributed to Litestar, also criticized the complexity of its DI system. So this was not only my impression after one unsuccessful test.

Today, I Would Look Toward a Shared Container

That is why I now see this solution as a temporary workaround rather than a pattern worth carrying forward.

If the application exists entirely inside Litestar and has only a couple of endpoints, the built-in DI system may be enough. But if there are HTTP endpoints, message brokers, background jobs, CLI commands, and shared use cases, it is better to move dependency assembly into one place. The API layer then does not decide how to create a use case. It only receives and calls it.

This is where tools such as anydi, Dishka, and similar containers become relevant. Dishka now has a proper Litestar integration: dependencies can be received through FromDishka, handlers can be marked with @inject, DishkaRouter can provide automatic injection, and the container can be connected through setup_dishka.

The point is not that a container magically makes the code clean. It simply moves the wiring out of the framework and into a separate layer. That is convenient for testing: you can create a test container with mocked use cases and connect it to Litestar, FastAPI, FastStream, or another entry point in the same way.

I have written about this idea separately in [[Использование общего IOC-контейнера|Using a Shared IoC Container]].

Conclusion

The old solution with Provide and a session-scoped mock worked. It allowed me to test the API contract without testing the use case where I did not want to test it.

Today, however, I would be more cautious about it. It is a useful sign that framework-level DI has started leaking into the test architecture. You can live with that once. If it keeps happening, it may be easier to admit that the application needs a separate composition root.

There may have been a simpler solution, but I no longer remember it. What is clearer now is why the problem appeared in the first place: I was trying to use the framework's DI system as the general assembly mechanism for the entire application, and it is not always well suited to that job.

Things to Verify Manually Before Publishing

  • The Litestar version used in the old side project. This determines whether the example should remain historical or be updated to the current API with NamedDependency and SkipValidation.
  • Whether the complete test class should be included. For now, I have kept only the shared-state cleanup fragment so the article does not turn back into a wall of code.
  • A link to Ilya Sobolev's specific comments about Litestar's DI system if the article is published publicly. At the moment, this remains the author's recollection rather than a direct quotation.