Dependency Injection

A technique to easily deal with dependencies

Dependency injection is a technique to deal with the dependency of one object to another. Classes don’t need to know how to instantiate their dependencies; they get passed in via the constructor or a method argument. This leads to more straightforward tests. Take a look at this example:

import os

import shotgun_api3


class ThumbnailGenerator(object):
    def __init__(self):
        self._sg = shotgun_api3.Shotgun(
            os.environ["SHOTGUN_URL"],
            script_name=os.environ["SHOTGUN_SCRIPT_NAME"],
            api_key=os.environ["SHOTGUN_API_KEY"],
        )

    def generate(self, entity_type, entity_id):
        path = "some_path"
        self._sg.upload_thumbnail(entity_type, entity_id, path)


if __name__ == "__main__":
    thumbnail_generator = ThumbnailGenerator()
    thumbnail_generator.generate("Asset", 1)

The corresponding test:

import mock

from di_injection.without_di_injection import ThumbnailGenerator


@mock.patch("shotgun_api3.Shotgun")
def test_thumbnail_uploader(sg_constructor_mock):
    thumbnail_generator = ThumbnailGenerator()
    with mock.patch.object(thumbnail_generator, "_sg") as mock_sg_instance:
        thumbnail_generator.generate("Asset", 1)

        mock_sg_instance.upload_thumbnail.assert_called_with("Asset", 1, "some_path")

In tests, we don’t want to test against a real shotgun instance. Instead, we want to use a mock. We have to use mock.patch to mock the shotgun instantiation and the _sg instance in ThumbnailGenerator. We can do better and easier with dependency injection:

import os

import shotgun_api3


class ThumbnailGenerator(object):
    def __init__(self, sg):
        self._sg = sg

    def generate(self, entity_type, entity_id):
        path = "some_path"
        self._sg.upload_thumbnail(entity_type, entity_id, path)


if __name__ == "__main__":
    sg = shotgun_api3.Shotgun(
        os.environ["SHOTGUN_URL"],
        script_name=os.environ["SHOTGUN_SCRIPT_NAME"],
        api_key=os.environ["SHOTGUN_API_KEY"],
    )
    thumbnail_generator = ThumbnailGenerator(sg)
    thumbnail_generator.generate("Asset", 1)

ThumbnailGenerator does not create a Shotgun instance anymore. Instead, it receives a ready Shotgun instance via its constructor.

Now the test looks like this:

import mock

from di_injection.with_di_injection import ThumbnailGenerator


def test_thumbnail_uploader():
    sg_mock = mock.MagicMock()
    thumbnail_generator = ThumbnailGenerator(sg_mock)

    thumbnail_generator.generate("Asset", 1)

    sg_mock.assert_called_with("Asset", 1, "some_path")

Dependency injection makes testing easier and avoids using mock.patch. In this example, ThumbnailGenerator also does not need to know where to get Shotgun credentials from.

Dependency injection works great in combination with abstractions. This leads to cleaner code which is easy to extend. We will discuss this in section Dealing with Depedencies.

Why just using mock.patch is a bad idea

You might argue, “I don’t need dependency injection, it’s too much overhead, I can just mock.patch to make code testable”. This is ok for legacy code. But using mock.patch usually indicates bad design. Some parts of your code can not be easily exchanged. It would be best if you thought about your code and architecture to avoid mock.patch, so you separate dependencies and access them using dependency injection. This will make your code easier to extend.

Dependency Injection frameworks

There are frameworks out there to support you with dependency injection by taking care of correctly construcing classes with their dependencies. An example for Python is pinject. But you don’t have to use a dependency injection framework, which adds additional complexity. You can do it on your own in main, as in ThumbnailGenerator example:

if __name__ == "__main__":
    sg = shotgun_api3.Shotgun(
        os.environ["SHOTGUN_URL"],
        script_name=os.environ["SHOTGUN_SCRIPT_NAME"],
        api_key=os.environ["SHOTGUN_API_KEY"],
    )
    thumbnail_generator = ThumbnailGenerator(sg)
    thumbnail_generator.generate("Asset", 1)


Last modified September 20, 2020