25

Python is a relatively new language for me. Unit Testing and Dependency Injection are something that I've been doing for a little while now, so I'm familiar with it from a C# perspective.

Recently, I wrote this piece of Python code:

import requests  # my dependency: http://docs.python-requests.org/en/latest/

class someClass:
    def __init__(self):
        pass

    def __do(self, url, datagram):
        return requests.post(self, url, datagram)

And then I realized that I had just created a hard-coded dependency. Bleh.

I had considered changing my code to do "Constructor" Dependency Injection:

def __init__(self,requestLib=requests):
    self.__request = requestLib

def __do(self, url, datagram):
    return self.__request.post(self, url, datagram)

This now allows me to inject a fake/mock dependency for the sake of Unit Testing, but wasn't sure if this was considered Python-ic. So I'm appealing to the Python community for guidance.

What are some examples of Python-ic ways to do basic DI (mostly for the sake of writing Unit Tests that utilize Mocks/Fakes)?

ADDENDUM For anyone curious about the Mock answer, I decided to ask a separate question here: How does @mock.patch know which parameter to use for each mock object?

Community
  • 1
  • 1
Pretzel
  • 8,141
  • 16
  • 59
  • 84
  • 2
    Note that `__leading_double_underscore` invokes name mangling, and should generally be avoided. Wouldn't it be easier to [`mock` out `requests`](https://docs.python.org/3/library/unittest.mock.html) for the module under test than inject it? – jonrsharpe Oct 26 '15 at 15:08
  • I read that the __leading double underscore was to mark a method private. Was I mistaken? If so, how should I mark something private? – Pretzel Oct 26 '15 at 15:23
  • Would it be easier to mock out requests for the module rather than inject it? I don't know. I'm not familiar with Python ways of doing things, which is why I'm asking. ;) – Pretzel Oct 26 '15 at 15:26
  • 1
    `_leading_single_underscore` is *private-by-convention* (see http://www.python.org/dev/peps/pep-0008/) - nothing is ever truly private in Python, though, and even name-mangled attributes are accessible if you're determined. We're all consenting adults! – jonrsharpe Oct 26 '15 at 15:26
  • C# [has mocking, too](https://msdn.microsoft.com/en-gb/library/ff650441.aspx)! There's a C# explanation of mocking vs. injection [here](http://stackoverflow.com/a/5433231/3001761). – jonrsharpe Oct 26 '15 at 15:28
  • 1
    Ahh, single underscore. My bad. I'm still learning here. Thanks for your patience. Yeah, the Python way of doing things is quite different than what I'm used to. Yeah, I read about that "consenting adults" thing and laughed. – Pretzel Oct 26 '15 at 15:29
  • Yes, I know. I've used both Rhino Mocks and Moq before. I'm just feeling out the Python way of doing things. – Pretzel Oct 26 '15 at 15:34
  • I wonder if this question can be updated with current context due to the popularity of FastAPI. – Jared Apr 09 '21 at 18:14

2 Answers2

16

Don't do that. Just import requests as normal and use them as normal. Passing libraries as arguments to your constructors is a fun thing to do, but not very pythonic and unnecessary for your purposes. To mock things in unit tests, use mock library. In python 3 it is built into the standard library

https://docs.python.org/3.4/library/unittest.mock.html

And in python 2 you need to install it separately

https://pypi.python.org/pypi/mock

Your test code would look something like this (using python 3 version)

from unittest import TestCase
from unittest.mock import patch

class MyTest(TestCase):
    @patch("mymodule.requests.post")
    def test_my_code(self, mock_post):
        # ... do my thing here...
Mad Wombat
  • 14,490
  • 14
  • 73
  • 109
  • 3
    So I'm trying to parse your code. What does the '@patch("mymodule.requests") do? And why are you passing in a "requests" to the "test_my_code" method. Also, what would the mock for "requests.post" look like? – Pretzel Oct 26 '15 at 15:40
  • 2
    The patch decorator substitutes requests module in your code for a mock. My code is most likely incorrect, you would have to patch every function in requests separately to make it work. The patched function is passed as an argument to the test, so you can do asserts against it. Read the docs for unittest and mock for more info on their usage. – Mad Wombat Oct 26 '15 at 16:07
  • Thanks for the help. I gave you the check mark. :) – Pretzel Oct 26 '15 at 20:14
7

While injecting the requests module can be a bit too much, it is a very good practice to have some dependencies as injectable.

After years using Python without any DI autowiring framework and Java with Spring I've come to realize that plain simple Python code often doesn't need a framework for dependency injection with autowiring (autowiring is what Guice and Spring both do in Java), i.e., just doing something like this may be enough:

def foo(dep = None):  # great for unit testing!
    ...

This is pure dependency injection (quite simple) but without magical frameworks for automatically injecting them for you. The caller has to instantiate the dependency or you can do it like this:

def __init__(self, dep = None):
    self.dep = dep or Dep()

As you go for bigger applications this approach won't cut it though. For that I've come up with injectable a micro-framework that wouldn't feel non-pythonic and yet would provide first class dependency injection autowiring.

Under the motto Dependency Injection for Humans™ this is what it looks like:

# some_service.py
class SomeService:
    @autowired
    def __init__(
        self,
        database: Autowired(Database),
        message_brokers: Autowired(List[Broker]),
    ):
        pending = database.retrieve_pending_messages()
        for broker in message_brokers:
            broker.send_pending(pending)
# database.py
@injectable
class Database:
    ...
# message_broker.py
class MessageBroker(ABC):
    def send_pending(messages):
        ...
# kafka_producer.py
@injectable
class KafkaProducer(MessageBroker):
    ...
# sqs_producer.py
@injectable
class SQSProducer(MessageBroker):
    ...
Rodrigo Oliveira
  • 1,452
  • 4
  • 19
  • 36