Search code examples
pythonunit-testingmockingpython-mock

Can a python constructor be mocked without mocking other properties of the object?


Is it possible to mock a python constructor while continuing to use the production version other fields/functions on the same name? For example, given the production code:

class MyClass:
    class SubClass:
        def __init__(self) -> None:
            print("\nreal sub init called")

        class SubSubClass:
            def __init__(self) -> None:
                print("\nreal sub sub init called")

and the following test code:

class FakeSubClass:
    def __init__(self) -> None:
        print("\nfake init called")


def test():
    MyClass.SubClass()
    MyClass.SubClass.SubSubClass()

    MyClass.SubClass = Mock(side_effect=FakeSubClass)

    MyClass.SubClass()
    MyClass.SubClass.SubSubClass()

we get the following output:

real sub init called

real sub sub init called

fake init called

Note that the last line MyClass.SubClass.SubSubClass() did not create a real SubSubClass because at this point it is an automatically created property of the SubClass mock.

My desired output is the following:

real sub init called

real sub sub init called

fake init called

real sub sub init called

In other words I want to mock ONLY the SubClass, but not SubSubClass. Things I have tried in place of the mocking line above (both of which do not work):

MyClass.SubClass.__init__ = Mock(side_effect=FakeSubClass.__init__)

MyClass.SubClass.__new__ = Mock(side_effect=FakeSubClass.__new__)

Note that I am aware of several ways the code could be refactored to avoid this issue but sadly the code cannot be refactored.


Solution

  • You can also fake the class, the thing MyClass.SubClass.SubSubClass() wont work in your case is that MyClass.SubClass is a Mock dont have SubSubClass definition. Simply let FakeSubClass inherit it from MyClass.SubClass will solve the issue.

    And you can easily patch MyClass to FakeClass, and you will have the right test object rather than real object.

    from unittest.mock import Mock
    
    
    class MyClass:
        class SubClass:
            def __init__(self) -> None:
                print("\nreal sub init called")
    
            class SubSubClass:
                def __init__(self) -> None:
                    print("\nreal sub sub init called")
    
    
    class FakeSubClass(MyClass.SubClass, Mock):
        def __init__(self) -> None:
            print("\nfake init called")
    
    
    class FakeClass:
        class SubClass(FakeSubClass):
            pass
    
    
    def test():
        MyClass.SubClass()
        MyClass.SubClass.SubSubClass()
    
        MyClass.SubClass = Mock(side_effect=FakeSubClass)
    
        FakeClass.SubClass()
        FakeClass.SubClass.SubSubClass()