unittest mock replace/reset 修补对象时的模拟函数

unittest mock replace/reset mocked function when patching an object

我需要使用 unittest.mock.patch.object 模拟有时可能会失败的外部方法。在测试中,该方法将引发一些错误,然后 return 到原始行为。请注意,我要重现的行为比 return 'bar' 复杂得多,所以我不能只复制 Bar.some_method_that_may_fail:

中的代码
import unittest
from unittest.mock import patch


class Bar(object):

    def some_method_that_may_fail(self):
        return "bar"


class Foo(object):
    bar = None

    def retry_method(self):
        try:
            self.__class__.bar = Bar().some_method_that_may_fail()
        except KeyError:
            self.retry_method()


class TestRetry(unittest.TestCase):

    def setUp(self):
        self.instance = Foo()

    def test_retry(self):
        # raise KeyError the first 5 calls to the function and then work normally
        errors_list = [KeyError("")] * 5

        def raise_errors(*_):
            if errors_list:
                errors_list.pop(0)
            # TODO: return to original behaviour

        with patch.object(Bar, 'some_method_that_may_fail', new=raise_errors) as mocked:
            self.instance.retry_method()
        self.assertEqual(self.instance.bar, 'bar')


if __name__ == '__main__':
    unittest.main()

要return 后续调用的不同值,您可以使用side_effect。传递值数组 and/or 异常将 return 这些 values/raise 这些异常在后续调用中,在您的情况下是异常实例和原始调用的结果(如果这是您需要的) . 所以你的测试看起来像这样:

class TestRetry(unittest.TestCase):

    def setUp(self):
        self.instance = Foo()

    def test_retry(self):
        original = Bar.some_method_that_may_fail  # save the original
        with patch(__name__ + '.Bar') as mocked:
            bar = mocked.return_value
            side_effect = ([KeyError()] * 5) + [original(bar)]
            bar.some_method_that_may_fail.side_effect = side_effect
            self.instance.retry_method()
            self.assertEqual(6, mocked.call_count)
        self.assertEqual('bar', self.instance.bar)

一些注意事项:

  • 我把mock.patch.object换成了mock.patch,因为你没有要patch的对象(Bar是在被测函数内部实例化的,需要patch实例,不是 class)
  • 使用 __name__ + '.Bar' 进行修补是因为被测试的函数与测试函数在同一个模块中 - 在实际代码中,必须将其替换为正确的模块路径
  • 我添加了一个检查 call_count 以确保该方法确实被调用了 6 次

另一件事:您的 Foo 中有错误,可能是因为您将其简化为示例。 foo 是一个 class 变量,但您正在设置一个同名的实例变量。您需要:

class Foo:
    bar = None

    def retry_method(self):
        try:
            self.__class__.bar = Bar().some_method_that_may_fail()
        except KeyError:
            self.retry_method()

(注意 self.__class__.bar