如何使用Mock测试含副作用的递归DAG深度优先遍历函数
解决递归DFS函数的Mock测试问题
首先,你的核心问题是没有正确Mock掉other_class.get_next_object_from_dep这个外部依赖,导致测试时要么调用了真实逻辑,要么Mock的配置不符合递归调用的需求。下面我会一步步教你怎么用unittest.mock来搞定这个测试。
关键思路
我们需要:
- 用
patch替换掉OtherClass的get_next_object_from_dep方法,让它返回我们预设的Mock对象,而不是真实调用外部服务/逻辑。 - 配置Mock的返回规则:因为递归过程中,第一次调用会传入子节点的依赖(指向父节点),所以Mock需要根据传入的
dep参数,返回对应的父节点Mock对象。
修改后的测试代码示例
import unittest from unittest.mock import patch, Mock import json # 假设你的真实类定义(根据你的示例补充) class ParentClass: def __init__(self, data): self.id = data["id"] self.dependencies = [] # 父节点没有依赖 class ChildClass: def __init__(self, data): self.id = data["id"] self.dependencies = [data["parent_id"]] # 子节点依赖父节点的ID class OtherClass: def __init__(self, session): self.session = session def get_next_object_from_dep(self, dep): # 真实逻辑:根据dep获取对象,测试时我们要Mock这个 pass # 你的递归DFS函数 def recursive_function(other_class, object_to_check, list_to_return=None): if list_to_return is None: list_to_return = [object_to_check] dependencies = object_to_check.dependencies if dependencies : for dep in dependencies: next_object = other_class.get_next_object_from_dep(dep) list_to_return.append(next_object) recursive_function(other_class, next_object, list_to_return) return list_to_return MOCK_PARENT = '''{"id": 1337}''' MOCK_CHILD = '''{"id": 1, "parent_id": 1337}''' class TestGraphTraversal(unittest.TestCase): def setUp(self): # 创建测试用的Mock对象 self.mock_parent_data = json.loads(MOCK_PARENT) self.mock_child_data = json.loads(MOCK_CHILD) self.parent_obj = ParentClass(self.mock_parent_data) self.child_obj = ChildClass(self.mock_child_data) self.session = Mock() self.other_class = OtherClass(self.session) @patch.object(OtherClass, 'get_next_object_from_dep') def test_graph_traversal(self, mock_get_next): # 配置Mock:当传入父节点ID(1337)时,返回我们的父节点对象 mock_get_next.side_effect = lambda dep: self.parent_obj if dep == 1337 else None # 调用递归函数 result = recursive_function(self.other_class, self.child_obj) # 验证结果是否符合预期 expected = [self.child_obj, self.parent_obj] self.assertEqual(result, expected) # 额外验证:Mock方法被调用了一次(因为子节点只有一个依赖) mock_get_next.assert_called_once_with(1337) if __name__ == '__main__': unittest.main()
代码解释
@patch.object(OtherClass, 'get_next_object_from_dep'):这个装饰器会替换OtherClass类中的get_next_object_from_dep方法为一个Mock对象,测试结束后自动恢复原方法。Mock对象会作为参数mock_get_next传入测试函数。mock_get_next.side_effect:这里用lambda表达式设置了一个动态返回规则——当传入的dep是1337(父节点ID)时,返回我们预设的parent_obj。如果有多个不同的依赖,可以扩展这个逻辑(比如用字典映射:dep_map = {1337: self.parent_obj, ...}; return dep_map.get(dep))。递归测试验证:调用递归函数后,我们直接断言返回的列表是否和预期一致,同时还可以验证Mock方法的调用次数和参数,确保递归逻辑正确触发了外部调用。
常见问题排查
- 如果你遇到“Mock对象没有属性”的错误,大概率是因为你没有正确配置Mock的返回值,导致递归过程中
next_object是一个空Mock,而不是带有dependencies属性的对象。确保你的Mock返回对象(比如self.parent_obj)有dependencies属性(即使是空列表)。 - 如果递归没有终止,检查你的Mock返回对象的
dependencies是否为空——父节点的dependencies应该是空列表,这样递归到父节点时就会停止。
内容的提问来源于stack exchange,提问作者Christian Dahlberg




