Python单元测试子类继承父类构造参数Mock失效问题排查
问题背景
使用Python开展单元测试时,通过Mock机制模拟外部API调用。现有父类初始化时可接收携带URL配置信息的外部API客户端作为入参,子类继承父类部分方法实现。对子类方法做Mock时,传入模拟URL替代真实服务地址,但子类实际读取父类默认配置的真实URL发起请求;仅Mock父类时,断言结果完全依赖传入的模拟URL配置。
父类实现
class Parent: def __init__(self, id:str, item:str, item_client = ClientSettings(settings.config.api_url)): self.id = id self.item = item self.item_client = item_client async def main_method_in_parent(self): order_details = self.item_client.get_item_details(self.id) get_order_details = order_details["response"]["payload"]["orders"]["details"]
子类实现
class Child(Parent): def __init__(self, id:str, item:str, status:str = None): super().__init__(id, item) self.status = status async def method_in_child(self): await self.main_method_in_parent() order_status = self.item_client.get_order_status(self.status) get_status = order_status["response"]["result"]["orders"]["details"]["status"]
测试用例实现
mock_json = { "response": { "payload": { "orders": { "details": "order_name", "status": "shipped" } } } } class TestParent(unittest.TestCase): def test_parent_child(self): loop = asyncio.get_event_loop() mock_client_api = ClientSettings("dummy_url") mock_client_api.get_item_details = MagicMock(return_value=async_return(mock_json)) mock_client_api.get_order_status = MagicMock(return_value=async_return(mock_json)) test_parent = Parent(id="3dcff3", item="some_item", item_client=mock_client_api) test_child = Child(test_parent, status="shipped") loop.run_until_complete(test_child.method_in_parent()) loop.run_until_complete(test_child.method_in_child()) assert get_order_details == "order_name" assert get_status == "shipped" loop.close() if __name__ == "__main__": unittest.main()
报错现象
测试中父类对应的第一个断言可正常通过,但第二个针对get_status的断言执行失败,抛出如下错误:
AttributeError: 'ClientConnectorError' object has no attribute 'get'
调试确认Mock阶段父类可以正常读取传入的dummy_url模拟地址,但子类始终加载父类默认配置的真实API地址发起请求。
问题根因
一共3处核心代码疏漏直接导致该问题:
- 子类实例化参数传错:创建
Child实例时第一个参数传入了Parent类实例test_parent,但Child构造方法第一个参数要求传入字符串类型的id,且全程没有给Child传入构造好的mock_client_api。子类调用父类构造方法时只传了id和item两个参数,没有传入自定义客户端,自然会使用父类默认绑定真实URL的客户端发请求。 - 父类默认参数写法存在Python经典的可变默认参数陷阱:
item_client = ClientSettings(settings.config.api_url)这个默认值会在父类定义加载阶段就初始化完成,后续所有不传item_client的实例都会直接复用这个绑定了真实地址的客户端实例,不会重新读取配置。 - 测试用例本身存在方法名、变量访问错误:测试中调用的
test_child.method_in_parent()不存在,父类对应方法名是main_method_in_parent;且get_order_details、get_status是类方法内部的局部变量,测试用例在全局作用域直接访问会抛出NameError,根本拿不到方法内的执行结果。
修复方案
- 修正父类默认参数写法,避免类定义阶段就初始化真实客户端:
class Parent: def __init__(self, id:str, item:str, item_client = None): self.id = id self.item = item # 延迟初始化默认客户端,仅当未传入自定义客户端时才加载真实配置 self.item_client = item_client if item_client is not None else ClientSettings(settings.config.api_url) async def main_method_in_parent(self): order_details = self.item_client.get_item_details(self.id) # 把局部变量作为返回值,方便测试断言 return order_details["response"]["payload"]["orders"]["details"]
- 给子类增加
item_client入参透传,确保自定义Mock客户端可以传到父类:
class Child(Parent): def __init__(self, id:str, item:str, status:str = None, item_client = None): # 透传item_client参数给父类 super().__init__(id, item, item_client=item_client) self.status = status async def method_in_child(self): order_details = await self.main_method_in_parent() order_status = self.item_client.get_order_status(self.status) get_status = order_status["response"]["payload"]["orders"]["status"] # 返回结果供断言使用 return order_details, get_status
- 修正测试用例的实例化、方法调用逻辑:
class TestParent(unittest.TestCase): def test_parent_child(self): loop = asyncio.get_event_loop() mock_client_api = ClientSettings("dummy_url") mock_client_api.get_item_details = MagicMock(return_value=async_return(mock_json)) mock_client_api.get_order_status = MagicMock(return_value=async_return(mock_json)) # 正确实例化Child,传入所有需要的参数,包括mock客户端 test_child = Child(id="3dcff3", item="some_item", status="shipped", item_client=mock_client_api) # 调用正确的方法名,拿返回值做断言 details, status = loop.run_until_complete(test_child.method_in_child()) assert details == "order_name" assert status == "shipped" # 校验Mock方法调用参数符合预期 mock_client_api.get_item_details.assert_called_once_with("3dcff3") mock_client_api.get_order_status.assert_called_once_with("shipped") loop.close()
内容的提问来源于stack exchange,提问作者Rak1994
相关产品推荐
相关产品推荐

