如何在使用Python测试子类时模拟父类中调用的构造函数



我试图模拟一个对象,该对象在初始化时会进行一些昂贵的网络调用,但实例化它的父类无法识别单元测试中的补丁。

我已经看过一些类似的问题:

  • 1在我目前的情况下不涉及任何继承
  • 2具有类似的继承结构,但它模拟的是一个方法而不是构造函数

最小可再现性示例3

文件结构
--src
|-- resource.py
|-- family.py
|-- test_child.py
resource.py
class Resource:
def __init__(self):
print("Expensive network call")
def use_resource(self) -> str:
print("Another expensive network call")
return "bar"
家族.py
from resource import Resource
class Parent:
def __init__(self):
print("In the Parent constructor.")
self.resource = Resource()
def foo(self):
print("Parent's foo")
print(self.resource.use_resource())
class Child(Parent):
def __init__(self):
super().__init__()
print("In the Child constructor")

def foo(self):
print("Child's foo")
super().foo()
test_child.py
import unittest
from unittest.mock import patch
from family import Child
class TestChild(unittest.TestCase):
@patch('resource.Resource')
def test_foo(self, mock_resource):
mock_resource.return_value.use_resource.return_value = "mock_bar"
child = Child()
child.foo()
mock_resource.return_value.use_resource.assert_called_once()
unittest.main()

预期结果

由于我正在修补resource.Resource,所以我希望避免在Parent实例化Resource时通常发生的昂贵的网络调用。理论上,运行test_child.py的输出应该是这样的:

In the Parent constructor.
In the Child constructor
Child's foo
Parent's foo
mock_bar

实际结果

然而,尽管在测试中修补了resource.Resource,但Parent仍在实例化实际的Resource,如";昂贵的网络呼叫";输出中的消息和失败的CCD_ 8断言。

In the Parent constructor.
Expensive network call           # Should have been patched
In the Child constructor
Child's foo
Parent's foo
Another expensive network call   # Should have been patched
bar                              # Should have been patched
F
======================================================================
FAIL: test_foo (__main__.TestChild)
----------------------------------------------------------------------
Traceback (most recent call last):
File "[REMOVED]unittestmock.py", line 1342, in patched
return func(*newargs, **newkeywargs)
File "[REMOVED]test_child.py", line 13, in test_foo
mock_resource.return_value.use_resource.assert_called_once()
File "[REMOVED]unittestmock.py", line 886, in assert_called_once
raise AssertionError(msg)
AssertionError: Expected 'use_resource' to have been called once. Called 0 times.
----------------------------------------------------------------------
Ran 1 test in 0.005s

为了使ParentResource实例化使用模拟的Resource而不是实际的Resource,我需要更改什么

使用@patch(family.Resource)而不是@patch(resource.Resource)

有两个关键的想法可以解释你的补丁失败的原因:

  1. Parent类实例化Resource时,它不是直接实例化resource.Resource,而是实例化family模块的Resource导入
  2. 虽然您的family模块确实导入了resource.Resource(您确实正在对其进行修补(,但该导入发生在修补程序的之前,因为您在执行修补程序之前导入family模块

如果需要更多详细信息,请参阅unittest.mock文档。Medium的这篇文章也非常有助于理解Python中嘲讽的怪异之处。


演示

为了完整起见,这是您的test_child.py和更正。。。

import unittest
from unittest.mock import patch
from family import Child

class TestChild(unittest.TestCase):
@patch('family.Resource')  # Changed this line
def test_foo(self, mock_resource):
mock_resource.return_value.use_resource.return_value = "mock_bar"
# Alternatively, move `from family import Child` here.
child = Child()
child.foo()
mock_resource.return_value.use_resource.assert_called_once()
unittest.main()

并且测试输出与问题中的预期输出相匹配。

In the Parent constructor.
In the Child constructor
Child's foo
Parent's foo
mock_bar
.
----------------------------------------------------------------------
Ran 1 test in 0.004s
OK