Build an application with Clean Architecture while using DI + mock in Python

When implementing a web application using Python in business, I decided to introduce Clean Architecture in order to develop while separating concerns for each layer.

We've put together a set of practices for developing testable applications using Dependency Injection, which also serves to share knowledge with team members.

Clean Architecture

This time, the purpose is a sample using Python, so the explanation of Clean Architecture is simple.

CleanArchitecture (1).jpg (Quoted from The Clean Architecture)

Clean Architecture is of interest in Architecture proposed by Robert C. Martin in 2012 A design method for achieving separation. Hexagonal architecture (advocated in 2005) and onion architecture (advocated in 2008) are architectures with the same aim.

The main advantages are clarification of business logic (although it is not unique to Clean Architecture), independence from UI, DB and framework, and improvement of testability.

As written in the original text, it is not necessary to cut the layers according to the concentric circle sample, and you can increase or decrease the layers as needed, but this time I decided to cut the layers according to the textbook.

Gradual typing using typing

Gradual Typing has become established in dynamically typed languages these days, but Python can also benefit from typing during development by using typing.

Unlike TypeScript, type errors are not detected at build time, but with an IDE like PyCharm (IntelliJ IDEA), you can develop while benefiting from the type, so you can achieve a development experience comparable to static typing.

Clean Architecture incorporates the Dependency Inversion Principle, and the use of typing will be essential for achieving operations through the interface.

Dependency injection utilizing abstract classes

Python doesn't have an interface, but it does have abstract classes. Abstract classes can have implementations, but the important thing in the Dependency Inversion Principle is that they depend on abstractions, so abstract classes can also achieve that requirement.

Let's reverse the dependency of the Repository layer that the Usecase layer actually calls through an abstract class.

class SampleRepository(metaclass=ABCMeta):
    @abstractmethod
    async def get(self, resource_id: str) -> dict:
        raise NotImplementedError


class SampleRepositoryImpl(SampleRepository):
    async def get(self, resource_id: str) -> dict:
        return {"id": id}


class SampleUsecase:
    sample_repository: SampleRepository

    def __init__(self, repository: SampleRepository):
        self.sample_repository = repository

   def get(self, resource_id: str) -> dict:
        return asyncio.run(self.sample_repository.get(resource_id))


SampleUsecase(repository=SampleRepositoryImpl()).get()

At this time, the Repository that the Usecase layer knows is an abstract class and does not know its implementation. Also, it is possible to give an implementation to an abstract class, but doing so violates the law of dependency inversion, so by giving @abstractmethod, the concrete class has an implementation.

Dependency Injection is the key to Clean Architecture, but you can inject dependencies with so-called vanilla DI without using a DI container. (Of course, DI container is not unnecessary)

Typing type checking is powerful when injecting dependencies, and with IDE support you should feel little inconvenience with dynamically typed languages.

Unit test using mock

One of the advantages of Clean Architecture is testability. Partial mocking can be done easily, coupled with Dependency Injection. Also, take advantage of the powerful features of the Python standard ʻunit test` module.

test_sample.py


class SampleRepositoryMock(SampleRepository):
    async def get(self, resource_id: str) -> dict:
        raise NotImplementedError


class TestSampleUsecase(TestCase):
    def test_get(self):
        get_mock = AsyncMock(return_value={"id": "0002"})

        repository = SampleRepositoryMock()
        repository.get = get_mock
        usecase = SampleUsecase(repository=repository)

        self.assertEqual(usecase.get("0002"), {"id": "0002"})
        get_mock.assert_called_with("0002")

MagicMock is provided as a partial mock, and you can easily replace the method by using this. You can also verify that it was called.

In addition, ʻAsyncMock is also provided as an awaitable return value, which eliminates the need to generate a provisional ʻasync def to return a native coroutine or a low-level Future type variable. .. When communicating to the outside, native coroutines using ʻasync / await` are often used in Python these days, but this can also be handled only by standard modules.

And at the time of testing, Dependency Injection is used to insert a dummy Repository for testing into the Usecase layer and verify the operation of only the layer to be tested.

By the way, the same thing can be achieved by injecting a class instance that is actually used without preparing a dummy class and then replacing it using a partial mock. Which is not the correct answer for this, if you want to focus only on the layer under test, it is better to inject a dummy class, and if you are testing across layers you don't need a dummy.

The reason why it is desirable to be a dummy in the former case is that if you forget the mock, the test will pass depending on the layer on which it depends, and the dummy class that raises an exception if you do not use the mock is more reliable. This is because it can be detected.

Use dataclass in Entity layer

In Clean Architecture, business logic is given to the Entity layer. Entity reminds me of DDD, but it's not exactly the same as Entity in DDD, nor is it the same as Value Object.

According to the original text, "Entity is a set of data structures and functions that encapsulates business rules."

Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.

(Quoted from The Clean Architecture)

There is no strict regulation on what kind of property the entity should have, but personally, dataclass introduced from Python 3.7 ) I think it goes well with it.

@dataclass
class Article(frozen=True, eq=True)
  id: str
  body: str

article1 = Article(id="1", body="test")
article2 = Article(id="1", body="test")

#True by value-based identity verification(Eq by default=True is set)
article1 == article2 

#An error occurs because it is frozen and is an immutable object.
article1.id = "2" 

The functions that dataclass itself has are convenient by themselves, but in particular, properties such as frozen for giving invariants and ʻeq` for identity verification by value are expected behaviors for the Entity layer. (Of course, invariance and value comparison are not always required)

Also, not only in the Entity layer, it is troublesome to define the __eq__ method sequentially when verifying the identity of an object in a unit test, and the merit of using the data class will be great.

Samples have been published on Github

I posted the source of a simple web application that actually works on Github because it would be redundant to put all the sample code in the article.

I'm using Flask as a web framework, but according to the idea of Clean Architecture, only the Rest layer depends on the library, and the rest is almost vanilla Python code.

Unit test code is also included, so check it out if you're interested.

Finally

Python has become one of the most popular languages in recent years as machine learning has grown in popularity, and it seems that its use in the web domain is increasing accordingly.

With the advent of Gradual typing and dataclass, the dynamic typing language has fully covered the areas where the development experience was impaired, and with the addition of traditional productivity, it is also an attractive option for web applications. It has become.

I myself have been writing Python at the production level for a few months, but I hope it will be helpful for those who want to try Python in the future because I can make use of the experience I have cultivated in other languages.

Recommended Posts

Build an application with Clean Architecture while using DI + mock in Python
Build and try an OpenCV & Python environment in minutes using Docker
Meaning of using DI framework in Python
Create an image with characters in python (Japanese)
Solve simultaneous equations in an instant using Python
To automatically send an email with an attachment using the Gmail API in Python
Send an email with Excel attached in Python
Build an interactive environment for machine learning in Python
Build a machine learning application development environment with Python
[Python] Create an infrastructure diagram in 3 minutes using diagrams
Make an application using tkinter an executable file with cx_freeze
Build an environment for machine learning using Python on MacOSX
Basic authentication with an encrypted password (.htpasswd) in bottle with python
[Python] Create an event-driven web crawler using AWS's serverless architecture
Things to keep in mind when using Python with AtCoder
Things to keep in mind when using cgi with python.
Print PDF in Python on Windows: Use an external application
Morphological analysis using Igo + mecab-ipadic-neologd in Python (with Ruby bonus)
Quicksort an array in Python 3
Scraping with selenium in Python
[S3] CRUD with S3 using Python [Python]
Working with LibreOffice in Python
Using Quaternion with Python ~ numpy-quaternion ~
Debugging with pdb in Python
Creating an egg with python
[Python] Using OpenCV with Python (Basic)
Working with sounds in Python
Build python3 environment with ubuntu 16.04
Scraping with Tor in Python
Build python environment with direnv
Tweet with image in Python
Combined with permutations in Python
Web application with Python + Flask ② ③
Let's build git-cat with Python
Translate using googletrans in Python
Using Python mode in Processing
DI (Dependency Injection) in Python
Using OpenCV with Python @Mac
Web application with Python + Flask ④
Send using Python with Gmail
How to deal with python installation error in pyenv (BUILD FAILED)
How to create a heatmap with an arbitrary domain in Python
Object extraction in images by pattern matching using OpenCV with Python
I tried to make a todo application using bottle with python
Build a Python execution environment using GPU with GCP Compute engine
Implement ranking processing with ties in Python using Redis Sorted Set
How to write what to do when an application is first displayed in Qt for Python with Designer