You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python辅助函数中unittest.mock.patch未作用于实例的问题

关于unittest.mock.patch封装复用后失效的问题分析与解决

我在用unittest.mock.patch编写测试,初始代码运行正常,但想把补丁逻辑封装成方法复用,改写后测试失败了——调用的是真实方法而非被补丁的方法。

初始正常代码

class TestService:
  def test_patched(self):
    service = Service()
    with patch.object(service, "_send_to_third_party") as patch_send:
      with patch.object(service, "_notify_listeners") as patch_notify:
        service.do_something()
        patch_send.assert_called_once()
        patch_notify.assert_called_once()

改写后失效的代码

class TestService:
  def _patched_service(self):
    service = Service()
    with patch.object(service, "_send_to_third_party") as patch_send:
      with patch.object(service, "_notify_listeners") as patch_notify:
        return service, patch_send, patch_notify

  def test_patched(self):
    service, patch_send, patch_notify = self._patched_service()
    service.do_something()
    patch_send.assert_called_once()
    patch_notify.assert_called_once()

为简化问题,以下测试用例中的第四个test_patching_works_with_function执行失败,我想明确问题原因和可行解决方案:

from unittest.mock import patch

class ExampleClass:
    def __init__(self):
        self.value = 0

    def add(self, value):
        self.value += self._add(value)

    def subtract(self, value):
        self.value -= self._subtract(value)

    def what_is_my_value(self):
        return self.value

    def _add(self, value):
        return value

    def _subtract(self, value):
        return value

def patch_me(exa: ExampleClass):
    with patch.object(exa, '_add', return_value=99):
        with patch.object(exa, '_subtract', return_value=66):
            return exa

class TestPatchingWorks:

    def test_unpatched_works(self):
        exa = ExampleClass()
        exa.add(5)
        exa.subtract(2)
        assert exa.what_is_my_value() == 3

    def test_patching_works_with_patch_class(self):
        exa = ExampleClass()
        with patch.object(ExampleClass, '_add', return_value=30):
            with patch.object(ExampleClass, '_subtract', return_value=10):
                assert exa.what_is_my_value() == 0
                exa.add(5)
                assert exa.what_is_my_value() == 30
                exa.subtract(2)
                assert exa.what_is_my_value() == 20

    def test_patching_works_with_patch_instance(self):
        exa = ExampleClass()
        with patch.object(exa, '_add', return_value=40):
            with patch.object(exa, '_subtract', return_value=30):
                assert exa.what_is_my_value() == 0
                exa.add(5)
                assert exa.what_is_my_value() == 40
                exa.subtract(2)
                assert exa.what_is_my_value() == 10

    def test_patching_works_with_function(self):
        exa = ExampleClass()
        exa = patch_me(exa)
        assert exa.what_is_my_value() == 0
        exa.add(5)
        assert exa.what_is_my_value() == 99
        exa.subtract(2)
        assert exa.what_is_my_value() == 33

问题原因

patch.object作为上下文管理器,离开with代码块后会自动撤销补丁。在patch_me或_patched_service函数中,你在with块内返回了实例,但函数执行完毕后,with块的作用域结束,补丁已经被还原,后续调用实例方法时使用的是原始的真实实现。


可行解决方案

方案1:将封装方法改为上下文管理器(用yield代替return)

把封装补丁的方法改成生成器,作为上下文管理器使用,这样补丁的作用域会延伸到测试代码的with块内:

针对TestService的修改:

class TestService:
  def _patched_service(self):
    service = Service()
    with patch.object(service, "_send_to_third_party") as patch_send:
      with patch.object(service, "_notify_listeners") as patch_notify:
        yield service, patch_send, patch_notify

  def test_patched(self):
    with self._patched_service() as (service, patch_send, patch_notify):
      service.do_something()
      patch_send.assert_called_once()
      patch_notify.assert_called_once()

针对简化例子的修改:

def patch_me(exa: ExampleClass):
    with patch.object(exa, '_add', return_value=99):
        with patch.object(exa, '_subtract', return_value=66):
            yield exa

# 测试用例修改
def test_patching_works_with_function(self):
    exa = ExampleClass()
    with patch_me(exa) as patched_exa:
        assert patched_exa.what_is_my_value() == 0
        patched_exa.add(5)
        assert patched_exa.what_is_my_value() == 99
        patched_exa.subtract(2)
        assert patched_exa.what_is_my_value() == 33

方案2:用装饰器组合复用补丁

如果要在多个测试方法中复用补丁,可以直接组合@patch装饰器,或者自定义组合装饰器:

class TestService:
  @patch.object(Service, "_send_to_third_party")
  @patch.object(Service, "_notify_listeners")
  def test_patched(self, patch_notify, patch_send):
    service = Service()
    service.do_something()
    patch_send.assert_called_once()
    patch_notify.assert_called_once()

# 自定义复用装饰器
def patch_service_methods(func):
    return patch.object(Service, "_notify_listeners")(patch.object(Service, "_send_to_third_party")(func))

class TestService:
  @patch_service_methods
  def test_patched(self, patch_send, patch_notify):
    service = Service()
    service.do_something()
    patch_send.assert_called_once()
    patch_notify.assert_called_once()

  @patch_service_methods
  def test_another_patched(self, patch_send, patch_notify):
    # 其他测试逻辑
    pass

方案3:手动管理补丁生命周期

通过调用补丁对象的start()和stop()方法,在测试类的setUp和tearDown中统一管理补丁:

class TestService:
  def setUp(self):
    self.service = Service()
    self.patch_send = patch.object(self.service, "_send_to_third_party").start()
    self.patch_notify = patch.object(self.service, "_notify_listeners").start()

  def tearDown(self):
    self.patch_send.stop()
    self.patch_notify.stop()

  def test_patched(self):
    self.service.do_something()
    self.patch_send.assert_called_once()
    self.patch_notify.assert_called_once()

这种方法会让所有测试方法共享补丁逻辑,无需重复编写补丁代码。


内容的提问来源于stack exchange,提问作者GSP

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 00:50:55