testAAA_with_patch_from_function_outside_with测试失败原因及多mock.patch简化咨询
一、三个测试用例的行为差异原因
你遇到的问题核心是mock.patch上下文管理器两种使用方式的本质区别:
前两个测试成功的原因
在testAAA和testAAA_with_patch_from_function中,你使用了with ... as patch_bbb的写法:with mock.patch("test_example2.BBB") as patch_bbb: # 或者 with self.setup_patch() as patch_bbb:这里的
patch_bbb接收的是被patch后的mock类对象(也就是AAA.main_func里调用的BBB实际指向的对象)。此时设置patch_bbb.return_value = self.mock_bbb,是直接给生效的mock类配置返回实例,后续创建的BBB实例都会用你预设的self.mock_bbb,所以调用计数符合预期。第三个测试失败的原因
在testAAA_with_patch_from_function_outside_with中,你先把Patch实例赋值给patch_bbb,再用with patch_bbb:(未加as绑定):patch_bbb = self.setup_patch() with patch_bbb: patch_bbb.return_value = self.mock_bbb这里的
patch_bbb是Patch上下文管理器实例本身,而非被patch后的mock类。进入with块时,Patch已经完成激活,此时修改Patch实例的return_value不会同步到已经生效的mock类上——因为Patch的return_value是激活前用来配置mock的参数,激活后修改对当前生效的mock完全无效。因此
AAA.main_func里创建的BBB实例用的是mock默认生成的实例,不是你预设的self.mock_bbb,自然func1和func2的调用计数为0,断言失败。若要让该测试成功,需修改为:
def testAAA_with_patch_from_function_outside_with(self): patch_bbb = self.setup_patch() with patch_bbb as mock_bbb: # 绑定被patch后的mock类 mock_bbb.return_value = self.mock_bbb aaa = AAA() aaa.main_func() self.assertEqual(2, self.mock_bbb.func1.call_count) self.assertEqual(2, self.mock_bbb.func2.call_count)
二、批量patch的简化方案
针对你需要重复使用6个patch的场景,有几种可行的简化方式:
1. 使用mock.patch.multiple
可一次性patch多个目标,返回包含所有mock对象的字典,避免重复编写多个with:
def test_with_multiple_patches(self): with mock.patch.multiple( "test_example2.BBB", "test_example2.OtherClass1", "test_example2.OtherClass2", # 继续添加其他需要patch的目标 BBB=mock.create_autospec(BBB), OtherClass1=mock.create_autospec(OtherClass1), # 或用spec=True自动生成spec ) as mocks: # 配置各个mock的返回值或行为 mocks["BBB"].return_value = self.mock_bbb # 执行测试逻辑 aaa = AAA() aaa.main_func() # 断言逻辑
2. 用contextlib.ExitStack管理多个patch
编写辅助函数创建所有patch,用ExitStack统一管理,可动态添加任意数量的patch:
from contextlib import ExitStack def setup_all_patches(self): stack = ExitStack() # 逐个添加需要的patch,返回对应的mock对象 mock_bbb = stack.enter_context(mock.patch("test_example2.BBB")) mock_other1 = stack.enter_context(mock.patch("test_example2.OtherClass1")) mock_other2 = stack.enter_context(mock.patch("test_example2.OtherClass2")) # 统一配置mock的默认行为 mock_bbb.return_value = self.mock_bbb return stack, (mock_bbb, mock_other1, mock_other2) def test_with_exit_stack(self): with self.setup_all_patches()[0]: aaa = AAA() aaa.main_func() self.assertEqual(2, self.mock_bbb.func1.call_count)
ExitStack会自动管理所有patch的进入和退出,无需手动维护多个上下文。
3. 封装成装饰器
如果多个测试用例都需要相同的patch,可把这些patch封装成装饰器,直接应用到测试方法或类上:
def apply_common_patches(func): # 堆叠多个patch装饰器 decorated = mock.patch("test_example2.OtherClass2")(func) decorated = mock.patch("test_example2.OtherClass1")(decorated) decorated = mock.patch("test_example2.BBB")(decorated) return decorated # 在测试方法上使用 class TestAAA(unittest.TestCase): @apply_common_patches def test_with_decorator(self, mock_bbb, mock_other1, mock_other2): mock_bbb.return_value = self.mock_bbb aaa = AAA() aaa.main_func() self.assertEqual(2, self.mock_bbb.func1.call_count)
装饰器会按从下到上的顺序把mock对象作为参数传递给测试方法,注意参数顺序与patch顺序相反。
4. 使用unittest.TestCase的addCleanup方法
在setUp中创建并启动所有patch,用addCleanup确保测试后清理:
def setUp(self) -> None: self.mock_bbb = mock.create_autospec(BBB) # 创建并启动patch self.patch_bbb = mock.patch("test_example2.BBB") self.mock_bbb_class = self.patch_bbb.start() self.mock_bbb_class.return_value = self.mock_bbb # 添加其他patch self.patch_other1 = mock.patch("test_example2.OtherClass1") self.mock_other1_class = self.patch_other1.start() # 注册清理方法 self.addCleanup(self.patch_bbb.stop) self.addCleanup(self.patch_other1.stop) def test_with_setup_cleanup(self): aaa = AAA() aaa.main_func() self.assertEqual(2, self.mock_bbb.func1.call_count)
这种方式适合所有测试用例都需要相同patch的场景,setUp启动patch,addCleanup确保每个测试后停止,避免测试间污染。
内容的提问来源于stack exchange,提问作者ajb

