Don’t Mock It: Hack It

Python holds a deep, dark secret: it can be as unpredictable as Ruby if we let it. Even with the new typing1, the staggering flexibility of Python at runtime remains. We can change individual instances of classes to anything we want, and nothing can stop us

But It Would Be Wrong #

The whole point of classes and instances is that they are predictable: every instance should act the same; every subclass should do the same things, perhaps in slightly different ways2. Cleverly3 changing instance 47 to get around some restrictive class design might feel good, but it is a land-mine you planted waiting for the next developer to come along

Except #

Except for testing. Testing less than end-to-end involves making some code think that it’s in the real world when it is not. Be it mocks,, stubs, composition, or references to services, tests need to simulate other code to simplify, isolate, and control the inputs and environment of the code under test. A common way is to create a simulacrum or stub of the other code that will dutifully return specified values and record how the code under test called it. Python’s MagicMock is a great example. It automatically creates methods on-demand and allows explicit return values and side-effects

Patching allows one to mock a reference that the test under code will use. For example, if one wants to test a configuration subsystem, one can patch the getenv reference to simulate different environmental variable values without having to actually change the environment. And the patch will automatically remove itself at the end of a test so as not to affect anything else

However, mocking an instance is sometimes overkill. If one is testing the behaviour within an instance and the instance does not do much on initialization, one can hack a real instance for testing by changing some of the methods themselves. Not only is this simpler, but it avoids the danger that a mock might grow apart from the thing it is mocking

from typing import Dict

class FooDog:
    def am_i_ready(self) -> bool:
        return self._external_reference is not None and \
                   self._external_reference.is_connected()

    def _that_thing(self, key: str) -> Dict:
        return self._external_reference.get_this_thing(key)

    def get_value_proposition(self, prefix: str) -> Dict:
        if not self.am_i_ready():
            raise Exception("I'm not ready")

        data = self._that_thing(prefix)

        return {prefix: data}


test_foo = FooDog()
test_foo.am_i_ready = lambda : True
test_foo._that_thing = lambda key : {"data": key}

result = test_foo.get_value_proposition("prefix")
expected = {"prefix": {"data": "prefix"}}

print(result == expected)

This looks a lot like code with mocking except we are using an instance of the real class as the scaffolding to hold our “mock” functions. One can even return instances of MagicMock using these simple lambdas. With loosely-coupled and specialized methods, hacking allows most test scenarios, however some things argue against it:

[3]: which makes it a good candidate for refactoring towards a top-down design


  1. which I love 

  2. i.e. virtual methods 

  3. check the blog name again. I’ll wait 

 
2
Kudos
 
2
Kudos

Now read this

Don’t Worry About Anything, Just Call Us

Callbacks are great and always have been. Long ago, we didn’t know we were injecting control or inverting dependencies; we just used them. When closures appeared, I did not see a huge difference: a little state here, a little context... Continue →