Skip to content

Instantly share code, notes, and snippets.

@mahmoodkhan
Created February 28, 2021 01:36
Show Gist options
  • Save mahmoodkhan/038a5358e96323dfe532bff690a1724c to your computer and use it in GitHub Desktop.
Save mahmoodkhan/038a5358e96323dfe532bff690a1724c to your computer and use it in GitHub Desktop.
Dependency Injection in Python 3 with Testing
# Dependency Injection in Python 3 with Testing
class MessageFormatter:
def success(self, message):
return f"👍 {message}"
class MessageWriter:
def __init__(self):
self.message_formatter = MessageFormatter()
def write(self, message):
print(self.message_formatter.success(message))
def main():
message_writer = MessageWriter()
message_writer.write("Hello World!")
if __name__ == "__main__":
main()
"""
This code works perfectly fine, but what if the implementation of MessageWriter were to change to take constructor parameters in a future version? This would involve needlessly passing the parameters through all the classes. Also, how could we possibly write some meaningful unit tests for our write function, it doesn’t even return anything! Instantiating an object inside of a constructor or a method is a code smell that this class is too rigid in its implementation.
A long term consequence of rigid code is spaghetti code caused by dependencies being haphazardly wired all throughout the codebase making it nearly impossible to understand who owns what and what the true scope of the class is.
"""
# Refactoring to Include Dependency Inversion
# Instead of creating the MessageFormatter in the constructor for MessageWriter we can instead pass an instance of MessageFormatter into MessageWriter.
class MessageFormatter:
def success(self, message):
return f"👍 {message}"
class MessageWriter:
def __init__(self, message_formatter):
self.message_formatter = message_formatter
def write(self, message):
print(self.message_formatter.success(message))
def main():
message_formatter = MessageFormatter()
message_writer = MessageWriter(message_formatter)
message_writer.write("Hello World!")
if __name__ == "__main__":
main()
"""
The difference at first is subtle, but powerful. Now if we wanted to write unit tests for MessageWriter we can create a mocked instance of MessageFormatter and pass it as a parameter to the constructor of MessageWriter. This mock allows us to substitute the dependency because it’s not a concern of our unit test. The following example uses pytest and pytest-mock.
"""
import pytest
from message_writer import MessageWriter
@pytest.fixture
def mock_message_formatter(mocker):
return mocker.patch("message_formatter.MessageFormatter")
def test_write_calls_message_formatter_success_once_with_value(mock_message_formatter):
message_writer = MessageWriter(mock_message_formatter)
message = "Hello World!"
message_writer.write(message)
"""
This code is starting to look better. It’s less fragile, and possible to write concise and meaningful unit tests. Unfortunately, there is a hidden problem with this example. As this codebase grows, more dependencies will bubble up into the main() function causing it to get out-of-hand. Lastly, we’ll find that we’re passing dependencies all the way from the highest levels of the program to the lowest levels of the program. This is tedious, and can lead to some pretty gnarly constructors with lots of parameters.
"""
# Refactoring to Use Dependency Injection
"""
Many languages have dependency injection frameworks to allow creating a container object to manage dependencies and the specifics of their creation.
Start by creating a container class to register your dependencies (these registrations are called providers in the dependency-injector parlance), then inside your various classes, you call upon the container object to resolve dependencies for you.
There are many types of providers available to use depending on the situation. From the dependency-injector documentation:
Provider — base provider class.
Callable — provider that calls a wrapped callable on every call. Supports positional and keyword argument injections.
Factory — provider that creates new instance of specified class on every call. Supports positional and keyword argument injections, as well as attribute injections.
Singleton — provider that creates new instance of specified class on its first call and returns the same instance on every next call. Supports position and keyword argument injections, as well as attribute injections.
Object — provider that returns provided instance “as is”.
ExternalDependency — provider that can be useful for development of self-sufficient libraries, modules, and applications that require external dependencies.
Configuration — provider that helps with implementing late static binding of configuration options — use first, define later.
From my personal experience, using the Factory and Singleton providers tend to solve most use cases. Following is a demonstration of how to create a container object — aptly named Container below — followed by how to utilize the provider created in the container to resolve the MessageFormatter dependency.
"""
from dependency_injector import containers, providers
class MessageFormatter:
def success(self, message):
return f"👍 {message}"
class MessageWriter:
def __init__(self, message_formatter=None):
if message_formatter:
self.message_formatter = message_formatter
else:
self.message_formatter = Container.message_formatter()
def write(self, message):
print(self.message_formatter.success(message))
def main():
message_formatter = MessageFormatter()
message_writer = MessageWriter(message_formatter)
message_writer.write("Hello World!")
class Container(containers.DeclarativeContainer):
message_formatter = providers.Factory(MessageFormatter)
if __name__ == "__main__":
main()
"""
we use the Factory provider to define how to create a MessageFormatter object. Each time we call Container.message_formatter()
we get a new instance of MessageFormatter. The same unit test demonstrated earlier works as is for this last example. To make the above example work with our unit test, we had to implement an if-statement in the MessageWriter constructor. While this isn’t ideal, it’s a solution whose downsides are overshadowed by the advantages of dependency injection.
Now this code has the advantage of being more modular by being able to substitute concrete implementations, more testable by being able to mock out higher-level dependencies, and more concise by not having an egregious number of constructor parameters. Higher-level dependencies are no longer being woven through classes who don’t use the dependency directly. Little is worse for the long-term maintainability (and readability) of a codebase than having a reference to an object in a class that only exist to pass to a lower-level dependency. The dependency-injector framework is small, simple, and approachable; there is very little to it which makes adding it to any Python project straightforward and worthwhile.
https://github.com/mahmoodkhan/dependency_injection
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment