Skip to content

Commit

Permalink
Merge pull request #105 from reagento/develop
Browse files Browse the repository at this point in the history
v0.7.0
  • Loading branch information
Tishka17 committed Mar 19, 2024
2 parents 139b2c6 + 69aa5a6 commit 430c5ea
Show file tree
Hide file tree
Showing 71 changed files with 1,734 additions and 976 deletions.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,20 +182,34 @@ This allows you to have multiple parts of application build separately without n
class MyProvider(Provider):
a = provide(source=AChild, scope=Scope.REQUEST, provides=A)
```
* Having multiple interfaces which can be created as a same class with defined provider? Use alias:
* Having multiple interfaces which can be created as a same class? Use `AnyOf`:

```python
from dishka import AnyOf

class MyProvider(Provider):
@provide
def p(self) -> AnyOf[A, AProtocol]:
return A()
```

Use alias if you want to add them in another `Provider`:

```python
class MyProvider2(Provider):
p = alias(source=A, provides=AProtocol)
```
it works the same way as

In both cases it works the same way as

```python
class MyProvider(Provider):
class MyProvider2(Provider):
@provide(scope=<Scope of A>)
def p(self, a: A) -> AProtocol:
return a
```


* Want to apply decorator pattern and do not want to alter existing provide method? Use `decorate`. It will construct object using earlie defined provider and then pass it to your decorator before returning from the container.

```python
Expand Down Expand Up @@ -253,4 +267,4 @@ class MyProvider(Provider):
@provide
async def get_a(self) -> A:
return A()
```
```
3 changes: 0 additions & 3 deletions docs/advanced/testing.rst

This file was deleted.

19 changes: 19 additions & 0 deletions docs/advanced/testing/app_before.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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: Annotated[Connection, FromDishka()]) -> str:
connection.execute("select 1")
return "Ok"


app = FastAPI()
app.include_router(router)
17 changes: 17 additions & 0 deletions docs/advanced/testing/app_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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
21 changes: 21 additions & 0 deletions docs/advanced/testing/container_before.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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)
37 changes: 37 additions & 0 deletions docs/advanced/testing/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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)
44 changes: 44 additions & 0 deletions docs/advanced/testing/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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:

.. literalinclude:: ./app_before.py

And a container:

.. literalinclude:: ./container_before.py

First of all - split your application factory and and container setup.

.. literalinclude:: ./app_factory.py

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.

.. literalinclude:: ./fixtures.py

Write tests.

.. literalinclude:: ./sometest.py


Bringing all together
============================


.. literalinclude:: ./test_example.py
7 changes: 7 additions & 0 deletions docs/advanced/testing/sometest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from unittest.mock import Mock


async def test_controller(client: TestClient, connection: Mock):
response = client.get("/")
assert response.status_code == 200
connection.execute.assertCalled()
81 changes: 81 additions & 0 deletions docs/advanced/testing/test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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: Annotated[Connection, FromDishka()]) -> 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()
Loading

0 comments on commit 430c5ea

Please sign in to comment.