Python:在mock patch类装饰器上调用stop

7 投票
1 回答
6229 浏览
提问于 2025-04-18 15:42

Mock的文档介绍了一种简单而优雅的方法,可以将补丁应用到一个TestCase里的所有测试方法上:

@patch('foo.bar')
@patch('foo.baz')
@patch('foo.quux')
@patch('foo.narf')
class FooTest(TestCase):

    def test_foo(self, bar, baz, quux, narf):
        """ foo """
        self.assertTrue(False) 

不过,我在使用这种方法时遇到了一个问题,就是如果我想在某个测试方法里调用stop()来停止某个补丁,似乎没有办法获取到补丁对象的引用——传入方法的只有模拟对象,比如barbazquuxnarf

我找到的唯一解决这个问题的方法是,按照Mock文档中描述的模式,在TestCasesetUp方法里实例化并启动补丁,然后在tearDown方法里停止它。这虽然能满足我的需求,但增加了很多额外的代码,看起来没有使用类装饰器的方法那么优雅。

有没有其他方法可以解决这个问题呢?

1 个回答

9

1

假设你想在一个方法里暂时恢复 foo.narf。在这个被装饰的函数中,foo.narf 是一个 MagicMock 对象。这个对象有一个叫 _mock_wraps 的属性,当你调用这个 mock 对象时,它会被触发!所以在你的模块顶部,你可以写 _narf = foo.narf,然后在你的测试案例中,设置 foo.narf._mock_wraps = _narf

但这里有个问题,这样做只会让它通过到真正的函数,而不是把它真正换回来,这就意味着有些测试案例会失败(例如,如果它们依赖于函数对象实际上是“它自己”)。而且如果你的 mock 对象还有其他属性,这可能会造成干扰(我没有做太多测试),因为调用 _mock_wraps() 是在一个方法的底部,而这个方法首先会考虑 mock 的其他属性。

2

patch() 装饰器涉及到每个 patcher(每个方法都有独立的副本)被添加到一个叫 patchings 的列表中,这个列表是方法本身的一个字段。也就是说,你可以通过 self.test_foo.patchings 来访问这个列表,然后找到你想要的那个。

不过,当你把 patch() 当作装饰器使用时,start()stop() 实际上并不会被调用,一旦你开始深入修改它,行为就会变得复杂。所以我写了这个上下文管理器。

class unpatch:
    def __init__(self, name, method):
        compare = patch(name)
        self.patcher = next((
            p for p in method.patchings
            if p.target == compare.getter()
            and p.attribute == compare.attribute
        ), None)
        if self.patcher is None:
            raise ValueError(name)

    def __enter__(self):
        self.patcher.__exit__()

    def __exit__(self, *exc_info):
        self.patcher.__enter__()

在你的测试案例中,你可以这样使用它:

with unpatch('foo.narf', self.test_foo):
    foo.narf()

免责声明:这只是一些小技巧。

撰写回答