关于python:如何修补一个对象,以便所有方法都被模拟,除了一个?

How do I patch an object so that all methods are mocked except one?

我有一个入口点函数,它在一个对象上调用它main我想要保持未被模拟,因为它在对象上调用了几个其他方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Thing(object):

    def main(self):
        self.alpha()
        self.bravo()

    def alpha(self):
        self.charlie()

    def bravo(self):
        raise TypeError("Requires Internet connection!")

    def charlie(self):
        raise Exception("Bad stuff happens here!")

这很简单,可以手动模拟:

1
2
3
thing = Thing()
thing.alpha = MagicMock()
thing.bravo = MagicMock()

我可以测试以确保alpha和bravo都被调用一次,我可以在alpha和bravo中设置副作用以确保它们被处理等等。

我担心的是,如果代码定义发生变化,并且有人将charlie调用添加到main。它没有被嘲笑,所以现在会感觉到副作用(而且它们就像写入文件,连接到数据库,从Internet上获取东西,因此这个简单的异常不会提醒我现在的测试坏)。

我的计划是验证我的模拟对象不会调用其他方法而不是我应该说的方法(或者引发测试异常)。但是,如果我做这样的事情:

1
2
3
4
5
6
MockThing = create_autospec(Thing)
thing = Thing()
thing.main()

print thing.method_calls
# [calls.main()]

然后main也被模拟,因此它不会调用其他方法。我怎么能模仿每个方法但主要的方法呢? (我希望method_calls为[calls.alpha(), calls.bravo()])。

编辑:用于黑客答案

好吧,我有一个非常hacky的解决方案,但我希望有一个比这更好的答案。基本上我从原始类重新绑定方法(Python绑定一个未绑定的方法)

1
2
3
4
5
6
7
MockThing = create_autospec(Thing)
thing = MockThing()
thing.main = Thing.ingest.__get__(thing, Thing)
thing.main()

print thing.method_calls
# [calls.alpha(), calls.bravo()]

但是必须有一个比使用函数描述符更简单的解决方案!


当我做这些奇怪的事情,比如调用一个类的实际方法,我会模拟我用来调用方法静态引用:

1
2
3
4
5
mt = Mock(Thing)
Thing.main(mt)
print(mt.mock_calls)

[call.alpha(), call.bravo()]

在编写测试之后,最好通过使用一些协作者将您应该模拟的内容与要测试的内容分开来进行分离:使用这些测试来引导生产代码重构,最后重构您的测试以删除这些类型的脏测试。


我有同样的问题,但我找到了一种方法,我很满意。下面的示例使用您上面列出的Thing类:

1
2
3
4
import mock

mock_thing = mock.create_autospec(Thing)
mock_thing.main = lambda x: Thing.main(mock_thing, x)

这将导致mock_thing调用属于mock_thing的实际"main"函数,但mock_thing.alpha()和mock_thing.beta()都将被称为模拟! (将x替换为您传入函数的任何参数)。

希望这对你有用!


看起来你要么让单元测试进入你的集成/功能测试,要么你担心测试除了某个单元之外的其他东西。

如果你是单元测试Thing那么你应该嘲笑你没有测试的部分。但是如果你正在测试Thing如何与其他东西(例如数据库)集成,那么你应该测试你的实际Thing,而不是模拟的。

此外,这是依赖注入很有意义的地方,因为你会做这样的事情:

1
2
3
4
5
6
7
8
9
class Thing:
    def __init__(self, db, dangerous_thing):
        self.db = db
        self.dangerous_thing = dangerous_thing

    #....

    def charlie(self):
        foxtrot = self.dangerous_thing.do_it()

现在,您可以在测试Thing时传入dangerous_thing的模拟,这样您就不必担心真正执行dangerous_thing了。