You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何使用Mock测试含副作用的递归DAG深度优先遍历函数

解决递归DFS函数的Mock测试问题

首先,你的核心问题是没有正确Mock掉other_class.get_next_object_from_dep这个外部依赖,导致测试时要么调用了真实逻辑,要么Mock的配置不符合递归调用的需求。下面我会一步步教你怎么用unittest.mock来搞定这个测试。

关键思路

我们需要:

  • patch替换掉OtherClassget_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()

代码解释

  1. @patch.object(OtherClass, 'get_next_object_from_dep'):这个装饰器会替换OtherClass类中的get_next_object_from_dep方法为一个Mock对象,测试结束后自动恢复原方法。Mock对象会作为参数mock_get_next传入测试函数。

  2. mock_get_next.side_effect:这里用lambda表达式设置了一个动态返回规则——当传入的dep是1337(父节点ID)时,返回我们预设的parent_obj。如果有多个不同的依赖,可以扩展这个逻辑(比如用字典映射:dep_map = {1337: self.parent_obj, ...}; return dep_map.get(dep))。

  3. 递归测试验证:调用递归函数后,我们直接断言返回的列表是否和预期一致,同时还可以验证Mock方法的调用次数和参数,确保递归逻辑正确触发了外部调用。

常见问题排查

  • 如果你遇到“Mock对象没有属性”的错误,大概率是因为你没有正确配置Mock的返回值,导致递归过程中next_object是一个空Mock,而不是带有dependencies属性的对象。确保你的Mock返回对象(比如self.parent_obj)有dependencies属性(即使是空列表)。
  • 如果递归没有终止,检查你的Mock返回对象的dependencies是否为空——父节点的dependencies应该是空列表,这样递归到父节点时就会停止。

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

火山引擎 最新活动