This is something that I struggled with for a long time, but I think I finally found a solution.
As you already noticed, if you try to replace the base class with Mock, the class you are trying to test just becomes a layout that defeats your ability to test it. The solution is to make fun of only the methods of the base class, not the entire base class, but this is easier said than done: it can be quite error prone to smear each method based on the test every time.
Instead, I created a class that scans another class and assigns Mock() , which correspond to the methods of another class. Then you can use this class instead of the real base class when testing.
Here is a fake class:
class Fake(object): """Create Mock()ed methods that match another class methods.""" @classmethod def imitate(cls, *others): for other in others: for name in other.__dict__: try: setattr(cls, name, Mock()) except (TypeError, AttributeError): pass return cls
So, for example, you may have code like this (this is an apology that this is a little far-fetched, just assume that BaseClass and SecondClass do nontrivial work and contain a lot of methods, and you donβt even have to define them at all):
class BaseClass: def do_expensive_calculation(self): return 5 + 5 class SecondClass: def do_second_calculation(self): return 2 * 2 class MyClass(BaseClass, SecondClass): def my_calculation(self): return self.do_expensive_calculation(), self.do_second_calculation()
Then you can write some of these tests:
class MyTestCase(unittest.TestCase): def setUp(self): MyClass.__bases__ = (Fake.imitate(BaseClass, SecondBase),) def test_my_methods_only(self): myclass = MyClass() self.assertEqual(myclass.my_calculation(), ( myclass.do_expensive_calculation.return_value, myclass.do_second_calculation.return_value, )) myclass.do_expensive_calculation.assert_called_once_with() myclass.do_second_calculation.assert_called_once_with()
Thus, the methods that exist in the base classes remain available as mocks, with which you can interact, but your class does not become a layout by itself.
And I was careful to make sure that this works in both python2 and python3.