pythontestingmockingpytest

Mock superclass __init__ method or superclass as a whole for testing


I want to test a Python class I wrote, which is like the following:

from external_library import GenericClass

class SpecificClass(GenericClass):

  def __init__(self, a, b, c):
    super(SpecificClass, self).__init__(a, b)
    self.c = c

  def specific_method(self, d):
    self.initialize()
    return d + self.c + self.b + self.a.fetch()

GenericClass is defined by an external library (external_library):

class GenericClass(object):

  def __init__(self, a, b):
    self.a = a
    self.b = b + "append"

  def initialize(self):
    # it might be an expensive operation
    self.a.initialize()

  def specific_method(self, d):
    raise NotImplementedError

How should I test specific_method? Other than GenericClass.initialize, should I mock out GenericClass as a whole, GenericClass.__init__, super or GenericClass.a and GenericClass.b? Or my approach to the problem is totally wrong?

Please use mock as mocking library and pytest as test framework. The solution is required to be Python2.6+ compatible.

Thanks in advance.


Solution

  • It's a little bit hard to infer what the best approach might be from a synthetic example. When possible, use just the needed mocks, since using real objects as much as possible is the best approach. But it is a trade off game, and when creating real objects is difficult and dependencies are deep, then twisted mocks can be very a powerful tool. Moreover mocks can give you a better sense points where your test can play. There is a price to pay: sometimes by using mock you can hide bugs that can be raised only by integration tests.

    IMHO for your case the best approach is to just mock a and set fetch's return value. You can also test the call to a.initialize().

    If your target is to just mock a.initialize() (and of course a.fetch() since I assume it makes no sense without initialize()); the best thing is just patch a object.

    Anyway last test is about patching the superclass. In your case that is little tricky you must both patch __init__ and inject a, b attribute. If you are worry about what GenericClass do in init you must patch it directly __init__ method: patch the class is useless because its reference is already in SpecificClass definition (it is resolved by super and not by GenericClass.__init__(...)). Maybe in the real code you should fight against read only property for a and b: again if you can use real object and just patch less method/object as possible is the best choice. I love mock framework but some times use it is not the best deal.

    One more thing: I used patch.multiple just to have a more compact example, but patch the method by two patch.object do the same work.

    import unittest
    from mock import Mock, patch, DEFAULT, PropertyMock
    from specific import SpecificClass
    
    
    class A():
        def initialize(self):
            raise Exception("Too slow for tests!!!!")
    
        def fetch(self):
            return "INVALID"
    
    
    class MyTestCase(unittest.TestCase):
        def test_specific_method(self):
            a = A()
            with patch.multiple(a, initialize=DEFAULT, fetch=DEFAULT) as mocks:
                mock_initialize, mock_fetch = mocks["initialize"], mocks["fetch"]
                mock_fetch.return_value = "a"
                sc = SpecificClass(a, "b", "c")
                self.assertEqual("dcbappenda", sc.specific_method("d"))
                mock_initialize.assert_called_with()
    
        def test_sanity_check(self):
            a = A()
            sc = SpecificClass(a, "b", "c")
            self.assertRaises(Exception, sc.specific_method, "d")
    
        #You can patch it in your module if it is imported by from .. as ..
        @patch("specific.GenericClass.__init__", autospec=True, return_value=None)
        def test_super_class(self, mock_init):
            sc = SpecificClass("a", "b", "c")
            mock_init.assert_called_with(sc, "a", "b")
            #Inject a and b
            sc.a = Mock()
            sc.b = "b"
            #configure a
            sc.a.fetch.return_value = "a"
            self.assertEqual("dcba", sc.specific_method("d"))
            sc.a.initialize.assert_called_with()