Dependency Injection
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)