如何创建可调用start()和stop()的patch.object Mock?解决属性错误
我有一套Python单元测试套件(基于Django TestCase),包含20个测试类共300个测试用例,想通过Mixin在setUp中统一应用4个patch.object(禁用耗时操作),避免每个测试类重复写嵌套with语句。
原来用嵌套with的方式可行,但代码繁琐:
from django.test import TestCase from unittest.mock import patch class MyTestCase(TestCase): def test_stuff(self): with patch.object('MyClass1', 'mymethod1') as mock_method_1: with patch.object('MyClass2', 'mymethod2') as mock_method_2: with patch.object('MyClass3', 'mymethod3') as mock_method_3: with patch.object('MyClass4', 'mymethod4') as mock_method_4: # 执行测试逻辑 self.assertEqual(mock_method_4.call_count, 0)
于是我写了个Mixin,在setUp中创建补丁并启动:
class PatchMixin(): def setUp(self): self.mock_method_1 = patch.object(MyClass1, "mymethod1") self.mock_method_2 = patch.object(MyClass2, "mymethod2") self.mock_method_3 = patch.object(MyClass3, "mymethod3") self.mock_method_4 = patch.object(MyClass4, "mymethod4") self.mock_method_1.start() self.mock_method_2.start() self.mock_method_3.start() self.mock_method_4.start() super().setUp() def tearDown(self): self.mock_method_1.stop() self.mock_method_2.stop() self.mock_method_3.stop() self.mock_method_4.stop() super().tearDown(self) class MyTestCase(TestCase): def test_stuff(self): # 执行测试逻辑 self.assertEqual(self.mock_method_4.call_count, 0)
但运行时报错:
Failed: [undefined]AttributeError: '_patch' object has no attribute 'call_count'
错误原因
你当前的代码中,self.mock_method_4存储的是patch.object()返回的补丁控制器(_patch对象),而非被替换后的mock实例。call_count是mock对象的属性,补丁控制器本身没有这个属性,因此触发报错。
在with语句中,as mock_method_4拿到的是patch.start()返回的mock实例;但你直接将patch.object()的结果存在self中,仅调用了start()却未保存其返回值,这就是问题核心。
解决方案
方案1:保存start()返回的mock实例
修改Mixin,将补丁控制器和mock实例分开存储,确保self中保存的是实际的mock对象:
class PatchMixin(): def setUp(self): # 创建补丁控制器 self._patch1 = patch.object(MyClass1, "mymethod1") self._patch2 = patch.object(MyClass2, "mymethod2") self._patch3 = patch.object(MyClass3, "mymethod3") self._patch4 = patch.object(MyClass4, "mymethod4") # 启动补丁,保存返回的mock实例 self.mock_method_1 = self._patch1.start() self.mock_method_2 = self._patch2.start() self.mock_method_3 = self._patch3.start() self.mock_method_4 = self._patch4.start() super().setUp() def tearDown(self): # 通过补丁控制器停止补丁 self._patch1.stop() self._patch2.stop() self._patch3.stop() self._patch4.stop() super().tearDown()
测试用例中self.mock_method_4.call_count即可正常访问,因为此时存储的是mock实例。
方案2:用patch.multiple简化批量补丁
如果多个补丁都是针对不同类的方法,使用patch.multiple可以一次性创建并管理多个补丁,代码更简洁:
from unittest.mock import patch, MagicMock class PatchMixin(): def setUp(self): # patch.multiple同时打多个补丁,spec=True保留原方法签名 self.mocks = patch.multiple( MyClass1, mymethod1=MagicMock(), MyClass2, mymethod2=MagicMock(), MyClass3, mymethod3=MagicMock(), MyClass4, mymethod4=MagicMock(), spec=True ).start() super().setUp() def tearDown(self): patch.stopall() # 停止所有活跃补丁 super().tearDown()
测试用例中访问mock的方式:self.mocks['mymethod4'].call_count,无需手动管理每个补丁的启停。
方案3:用装饰器结合Mixin(自动管理补丁生命周期)
借助@patch装饰器给Mixin的setUp方法打补丁,自动处理补丁的启停,无需手动写tearDown:
from unittest.mock import patch class PatchMixin(): # 注意装饰器顺序:最后一个装饰器的参数最先传入setUp @patch.object(MyClass1, 'mymethod1') @patch.object(MyClass2, 'mymethod2') @patch.object(MyClass3, 'mymethod3') @patch.object(MyClass4, 'mymethod4') def setUp(self, mock_method4, mock_method3, mock_method2, mock_method1): self.mock_method_1 = mock_method1 self.mock_method_2 = mock_method2 self.mock_method_3 = mock_method3 self.mock_method_4 = mock_method4 super().setUp()
这种方式利用装饰器自动管理补丁的生命周期,适合Django TestCase的执行逻辑(每个测试用例都会调用setUp,装饰器会自动启动和停止补丁)。
内容的提问来源于stack exchange,提问作者thclark

