可以在不模拟对象的其他属性的情况下模拟 python 构造函数吗?

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

是否可以模拟 python 构造函数,同时继续使用同名其他 fields/functions 的生产版本?例如,给定生产代码:

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:
    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()

我们得到以下输出:

real sub init called

real sub sub init called

fake init called

请注意,最后一行 MyClass.SubClass.SubSubClass() 没有创建真正的 SubSubClass,因为此时它是一个自动创建的 属性 的 SubClass mock。

我想要的输出如下:

real sub init called

real sub sub init called

fake init called

real sub sub init called

换句话说,我只想模拟 SubClass,而不是 SubSubClass。我已经尝试代替上面的模拟行的东西(两者都不起作用):

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

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

请注意,我知道有几种重构代码的方法可以避免这个问题,但遗憾的是代码无法重构。

你也可以伪造 class,MyClass.SubClass.SubSubClass() 在你的情况下不起作用是因为 MyClass.SubClass 是一个没有 SubSubClass 定义的 Mock。只需让 FakeSubClass 从 MyClass.SubClass 继承它就可以解决问题。

并且您可以轻松地将 MyClass 修补为 FakeClass,并且您将拥有正确的测试对象而不是真实对象。

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()

我同意 ZhouQuan 的回答非常好,因为它适用于 MyClass.Subclass 上的任何方法或变量。也就是说,这里有一些可能有用也可能没用的变体。

如果 FakeSubClass 由于某种原因无法直接编辑,或者您只想继承 SubSubClass() 而没有别的,可以像这样在 test() 中进行更改。

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

    FakeSubClass.SubSubClass = MyClass.SubClass.SubSubClass()

    MyClass.SubClass = Mock(side_effect=FakeSubClass)

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

我认为可能值得注意的是 Mock 确实接受了一个 wraps 参数,它可以用来获得 similar 行为,尽管它是不完全是你要求的。这是一个例子。

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:
    def __init__(self) -> None:
        print("\nfake init called")

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

    MyClass.SubClass = Mock(side_effect=FakeSubClass, wraps=MyClass.SubClass)

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

这给出了不同的输出。

real sub init called

real sub sub init called

fake init called # A call to MyClass.SubClass() causes both real and fake inits.

real sub init called # Same MyClass.SubClass() call.

real sub sub init called # But now the SubSubClass() does resolve correctly.