Testing with dishka#
Testing you code does not always require the whole application to be started. You can have unit tests for separate components and even integration tests which check only specific links. In many cases you do not need IoC-container: you create objects with a power of Dependency Injection and not framework.
For other cases which require calling functions located on application boundaries you need a container. These cases include testing you view functions with mocks of business logic and testing the application as a whole. Comparing to a production mode you will still have same implementations for some classes and others will be replaced with mocks. Luckily, in dishka
your container is not an implicit global thing and can be replaced easily.
There are many options to make providers with mock objects. If you are using pytest
then you can
use fixtures to configure mocks and then pass those objects to a provider
create mocks in a provider and retrieve them in pytest fixtures from a container
The main limitation here is that a container itself cannot be adjusted after creation. You can configure providers whenever you want before you make a container. Once it is created dependency graph is build and validated, and all you can do is to provide context data when entering a scope.
Example#
Imagine, you have a service built with FastAPI:
from sqlite3 import Connection
from typing import Annotated
from fastapi import FastAPI, APIRouter
from dishka.integrations.fastapi import FromDishka, inject
router = APIRouter()
@router.get("/")
@inject
async def index(connection: FromDishka[Connection]) -> str:
connection.execute("select 1")
return "Ok"
app = FastAPI()
app.include_router(router)
And a container:
from collections.abc import Iterable
from sqlite3 import connect, Connection
from dishka import Provider, Scope, provide, make_async_container
from dishka.integrations.fastapi import setup_dishka
class ConnectionProvider(Provider):
def __init__(self, uri):
super().__init__()
self.uri = uri
@provide(scope=Scope.REQUEST)
def get_connection(self) -> Iterable[Connection]:
conn = connect(self.uri)
yield conn
conn.close()
container = make_async_container(ConnectionProvider("sqlite:///"))
setup_dishka(container, app)
First of all - split your application factory and container setup.
from fastapi import FastAPI
from dishka import make_async_container
from dishka.integrations.fastapi import setup_dishka
def create_app() -> FastAPI:
app = FastAPI()
app.include_router(router)
return app
def create_production_app():
app = create_app()
container = make_async_container(ConnectionProvider("sqlite:///"))
setup_dishka(container, app)
return app
Create a provider with you mock objects. You can still use production providers and override dependencies in a new one. Or you can build container only with new providers. It depends on the structure of your application and type of a test.
from sqlite3 import Connection
from unittest.mock import Mock
import pytest
import pytest_asyncio
from fastapi.testclient import TestClient
from dishka import Provider, Scope, provide, make_async_container
from dishka.integrations.fastapi import setup_dishka
class MockConnectionProvider(Provider):
@provide(scope=Scope.APP)
def get_connection(self) -> Connection:
connection = Mock()
connection.execute = Mock(return_value="1")
return connection
@pytest.fixture
def container():
container = make_async_container(MockConnectionProvider())
yield container
container.close()
@pytest.fixture
def client(container):
app = create_app()
setup_dishka(container, app)
with TestClient(app) as client:
yield client
@pytest_asyncio.fixture
async def connection(container):
return await container.get(Connection)
Write tests.
from unittest.mock import Mock
async def test_controller(client: TestClient, connection: Mock):
response = client.get("/")
assert response.status_code == 200
connection.execute.assertCalled()
Bringing all together#
from collections.abc import Iterable
from sqlite3 import Connection, connect
from typing import Annotated
from unittest.mock import Mock
import pytest
import pytest_asyncio
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from dishka import Provider, Scope, make_async_container, provide
from dishka.integrations.fastapi import FromDishka, inject, setup_dishka
router = APIRouter()
@router.get("/")
@inject
async def index(connection: FromDishka[Connection]) -> str:
connection.execute("select 1")
return "Ok"
def create_app() -> FastAPI:
app = FastAPI()
app.include_router(router)
return app
class ConnectionProvider(Provider):
def __init__(self, uri):
super().__init__()
self.uri = uri
@provide(scope=Scope.REQUEST)
def get_connection(self) -> Iterable[Connection]:
conn = connect(self.uri)
yield conn
conn.close()
def create_production_app():
app = create_app()
container = make_async_container(ConnectionProvider("sqlite:///"))
setup_dishka(container, app)
return app
class MockConnectionProvider(Provider):
@provide(scope=Scope.APP)
def get_connection(self) -> Connection:
connection = Mock()
connection.execute = Mock(return_value="1")
return connection
@pytest_asyncio.fixture
async def container():
container = make_async_container(MockConnectionProvider())
yield container
await container.close()
@pytest.fixture
def client(container):
app = create_app()
setup_dishka(container, app)
with TestClient(app) as client:
yield client
@pytest_asyncio.fixture
async def connection(container):
return await container.get(Connection)
@pytest.mark.asyncio
async def test_controller(client: TestClient, connection: Mock):
response = client.get("/")
assert response.status_code == 200
connection.execute.assert_called()