mock_constructor()

Let’s say we wan to unit test the Backup.delete method:

import storage

class Backup(object):
  def __init__(self):
    self.storage = storage.Client(timeout=60)

  def delete(self, path):
    self.storage.delete(path)

We want to ensure that when Backup.delete is called, it actually deletes path from the storage as well, by calling storage.Client.delete. We can leverage StrictMock and mock_callable() for that:

self.storage_mock = StrictMock(storage.Client)
self.mock_callable(self.storage_mock, 'delete')\
  .for_call('/file/to/delete')\
  .to_return_value(True)\
  .and_assert_called_once()
Backup().delete('/file/to/delete')

The question now is: how to put self.storage_mock inside Backup.__init__? This is where mock_constructor jumps in:

from testslide import TestCase, StrictMock, mock_callable
import storage
from backup import Backup

class TestBackupDelete(TestCase):
  def setUp(self):
    super().setUp()
    self.storage_mock = StrictMock(storage.Client)
    self.mock_constructor(storage, 'Client')\
      .for_call(timeout=60)\
      .to_return_value(self.storage_mock)

  def test_delete_from_storage(self):
    self.mock_callable(self.storage_mock, 'delete')\
      .for_call('/file/to/delete')\
      .to_return_value(True)\
      .and_assert_called_once()
    Backup().delete('/file/to/delete')

mock_constructor() makes storage.Client(timeout=60) return self.storage_mock. It is similar to mock_callable(), accepting the same call, behavior and assertion definitions. Similarly, it will also fail if storage.Client() (missing timeout) is called.

Note how by using mock_constructor(), not only you get all safe by default goodies, but also totally decouples your test from the code. This means that, no matter how Backup is refactored, the test remains the same.

Caveats

Because of the way mock_constructor() must be implemented (see next section), its usage must respect these rules:

  • References to the mocked class saved prior to mock_constructor() invocation can not be used, including previously created instances.
  • Access to the class must happen exclusively via attribute access (eg: getattr(some_module, "SomeClass")).

A simple easy way to ensure this is to always:

# Do this:
import some_module
some_module.SomeClass
# Never do:
from some_module import SomeClass

Note

Not respecting these rules will break mock_constructor() and can lead to unpredicted behavior!

Implementation Details

mock_callable() should be all you need:

self.mock_callable(SomeClass, '__new__')\
  .for_call()\
  .to_return_value(some_class_mock)

However, as of July 2019, Python 3 has an open bug https://bugs.python.org/issue25731 that prevents __new__ from being patched. mock_constructor() is a way around this bug.

Because __new__ can not be patched, we need to handle things elsewhere. The trick is to dynamically create a subclass of the target class, make the changes to __new__ there (so we don’t touch __new__ at the target class), and patch it at the module in place of the original class.

This works when __new__ simply returns a mocked value, but creates issues when used with .with_wrapper() or .to_call_original() as both requires calling the original __new__. This will return an instance of the original class, but the new subclass is already patched at the module, thus super() / super(Class, self) breaks. If we make them call __new__ from the subclass, the call comes from… __new__ and we get an infinite loop. Also, __new__ calls __init__ unconditionally, not allowing .with_wrapper() to mangle with the arguments.

The way around this, is to keep the original class where it is and move all its attributes to the child class:

  • Dynamically create the subclass of the target class, with the same name.
  • Move all __dict__ values from the target class to the subclass (with a few exceptions, such as __new__ and __module__).
  • At the subclass, add a __new__ that works as a factory, that allows mock_callable() interface to work.
  • Do some trickery to fix the arguments passed to __init__ to allow .with_wrapper() mangle with them.
  • Patch the subclass in place of the original target class at its module.
  • Undo all of this when the test finishes.

This essentially creates a “copy” of the class, at the subclass, but with __new__ implementing the behavior required. All things such as class attributes/methods and isinstance() are not affected. The only noticeable difference, is that mro() will show the extra subclass.

Test Framework Integration

TestSlide’s DSL

Integration comes out of the box for TestSlide’s DSL: you can simply do self.mock_constructor() from inside examples or hooks.

Python Unittest

testslide.TestCase is provided with off the shelf integration ready:

  • Inherit your unittest.TestCase from it.
  • If you overload unittest.TestCase.setUp, make sure to call super().setUp() before using mock_constructor().

Any Test Framework

You must follow these steps for each test executed that uses mock_constructor():

  • Integrate mock_callable() (used by mock_constructor under the hood).
  • After each test execution, you must unconditionally call testslide.mock_constructor.unpatch_all_callable_mocks. This will undo all patches, so the next test is not affected by them. Eg: for Python’s unittest: self.addCleanup(testslide.mock_constructor.unpatch_all_callable_mocks).
  • You can then call testslide.mock_constructor.mock_constructor directly from your tests.