【问题标题】:Python Mocking - How to obtain call arguments from a mock that is passed to another mock as a function argument?Python Mocking - 如何从作为函数参数传递给另一个模拟的模拟中获取调用参数?
【发布时间】:2022-10-05 13:18:59
【问题描述】:

我不确定这个问题的标题,因为用一句话来描述这个问题并不容易。如果有人可以建议一个更好的标题,我会编辑它。

考虑使用smbus2 与 I2C 设备通信的代码:

# device.py
import smbus2

def set_config(bus):
    write = smbus2.i2c_msg.write(0x76, [0x00, 0x01])
    read = smbus2.i2c_msg.read(0x76, 3)
    bus.i2c_rdwr(write, read)

我希望在不访问 I2C 硬件的情况下对此进行单元测试,尽我所能模拟 smbus2 模块(我尝试模拟整个 smbus2 模块,因此它甚至不需要安装,但是没有成功,所以我只好在测试环境中导入smbus2,即使它实际上并没有被使用——到目前为止没什么大不了的,我稍后会处理):

# test_device.py
# Depends on pytest-mock
import device

def test_set_config(mocker):
    mocker.patch('device.smbus2')
    smbus = mocker.MagicMock()

    device.set_config(smbus)

    # assert things here...
    breakpoint()

在断点处,我正在检查 pdb 中的 bus 模拟:

(Pdb) p smbus
<MagicMock id='140160756798784'>

(Pdb) p smbus.method_calls
[call.i2c_rdwr(<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>, <MagicMock name='smbus2.i2c_msg.read()' id='140160757050688'>)]

(Pdb) p smbus.method_calls[0].args
(<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>, <MagicMock name='smbus2.i2c_msg.read()' id='140160757050688'>)

(Pdb) p smbus.method_calls[0].args[0]
<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>

不幸的是,此时传递给write()read() 的参数已经丢失。它们似乎没有记录在smbus 模拟中,我无法在数据结构中找到它们。

有趣的是,如果我在writeread 分配之后中断set_config() 函数,并检查模拟模块, 我可以看到:

(Pdb) p smbus2.method_calls
[call.i2c_msg.write(118, [160, 0]), call.i2c_msg.read(118, 3)]

(Pdb) p smbus2.method_calls[0].args
(118, [160, 0])

因此,参数已作为method_call 存储在smbus2 模拟中,但没有复制到传递给函数的smbus 模拟中。

为什么不保留这些信息?有没有更好的方法来测试这个功能?


我认为这可以概括为:

In [1]: from unittest.mock import MagicMock

In [2]: foo = MagicMock()

In [3]: bar = MagicMock()

In [4]: w = foo.write(1, 2)

In [5]: r = foo.read(1, 2)

In [6]: bar.func(w, r)
Out[6]: <MagicMock name='mock.func()' id='140383162348976'>

In [7]: bar.method_calls
Out[7]: [call.func(<MagicMock name='mock.write()' id='140383164249232'>, <MagicMock name='mock.read()' id='140383164248848'>)]

请注意,bar.method_calls 列表包含对函数 .write.read 的调用(好),但缺少传递给这些函数的参数(坏)。这似乎破坏了此类模拟的有用性,因为它们不会像我预期的那样交互。有没有更好的方法来处理这个?

【问题讨论】:

  • set_config 的代码采用bus 参数,但似乎没有使用它——而是直接使用smbus 模块。这可能是个问题吗?
  • 你试过简单mocker.patch('smbus2')吗?
  • @SamuelDion-Girardeau 哎呀-不知何故,我粘贴的代码中缺少一行-抱歉!我现在会解决这个问题。缺少的行确实使用了bus
  • @Adrian 是的,我已经尝试过了,这会导致 readwrite 变量的 Mocks,这没关系,但它们不包含用于创建它们的参数,因为它们存储在 smbus2.i2c_msg代替模拟。因此它们与bus 模拟无关,并且测试无法以可靠的方式建立连接(smbus2 模拟可以通过测试进行检查,并且参数存在于其中,但是当进行了更多 i2c_msg.read/write 调用,没有指向此特定调用的链接,因此它没有用)。
  • @davidA 感谢您的更新,这现在更有意义了!刚刚放弃了一个答案,希望它有帮助。

标签: unit-testing dependency-injection mocking pytest pytest-mock


【解决方案1】:

我刚刚意识到您使用依赖注入并且您应该利用这一点。

  1. 这将是干净的方法。
  2. Mocks 的行为可能出人意料/令人讨厌(这并不意味着它们是邪恶的 - 只是在某个时候......违反直觉)

    我会推荐以下测试结构:

    # test_device.py
    import device
    
    def test_set_config():
        dummy_bus = DummyBus()
    
        device.set_config(dummy_bus)
    
        # assert things here...
        assert dummy_bus.read_data == 'foo'
        assert dummy_bus.write_data == 'bar'
    
        breakpoint()
    
    class DummyBus:
        def __init__(self):
            self.read_data = None
            self.write_data = None
    
        def i2c_rdwr(write_input, read_input):
            self.read_data = read_input
            self.write_data = write_input
    

【讨论】:

  • 谢谢你的回答。总的来说,我喜欢这种方法,并且我同意 Mocks 不能解决所有问题,但是您的答案并未完全涵盖细微差别 - 被测代码调用 smbus2.i2c_msg.writesmbus2.i2c_msg.read 来创建参数,理想情况下这些也会被嘲笑,因为它们是不平凡的数据结构。在我当前的代码中,我让set_config 调用smbus2 提供的函数,然后与自定义断言进行比较,但这很混乱,我更愿意找到一种方法来验证.read.write 是用正确的参数调用。
【解决方案2】:

您无法访问对writeread 的调用的原因是它们本身就是另一个模拟的return_value。您要做的是访问模拟“父级”(在此处使用术语:https://docs.python.org/3/library/unittest.mock.html)。

实际上可以访问父级,但我不确定这是一个好主意,因为它使用了MagicMock 对象_mock_new_parent 的未记录和私有属性。

def test_set_config(mocker):
    """Using the undocumented _mock_new_parent attribute"""

    mocker.patch('device.smbus2')
    smbus = mocker.MagicMock()

    device.set_config(smbus)

    # Retrieving the `write` and `read` values passed to `i2c_rdwr`.
    mocked_write, mocked_read = smbus.i2c_rdwr.call_args[0]

    # Making some assertions about how the corresponding functions were called.
    mocked_write._mock_new_parent.assert_called_once_with(0x76, [0x00, 0x01])
    mocked_read._mock_new_parent.assert_called_once_with(0x76, 3)

您可以改为使用一些虚假值来检查断言是否有效,并且您会看到 pytest 断言错误。

IMO 一种更简单、更标准的方法是直接查看来自模块模拟的调用:

def test_set_config_2(mocker):
    """ Using the module mock directly"""

    mocked_module = mocker.patch('device.smbus2')
    smbus = mocker.MagicMock()

    device.set_config(smbus)

    mocked_write = mocked_module.i2c_msg.write
    mocked_read = mocked_module.i2c_msg.read

    mocked_write.assert_called_once_with(0x76, [0x00, 0x01])
    mocked_read.assert_called_once_with(0x76, 3)

【讨论】:

  • 谢谢您的回答。我认为更标准的方法可能是最好的方法。复杂的是,还有其他对.read.write 的调用妨碍了我特别关注的那些,但也许通过明智地使用reset_mock 我可以充分隔离它们。我会坚持...
【解决方案3】:

对于遇到这个问题的任何人,我在另一个问题中提出了这个问题的变体,结果非常令人满意:

https://stackoverflow.com/a/73739343/

简而言之,创建一个派生自MagicMockTraceableMock 类,它返回一个跟踪其父级的新模拟,以及导致该模拟被创建的函数调用的参数。总之,有足够的信息来验证是否调用了正确的函数,并提供了正确的参数。

【讨论】:

    猜你喜欢
    • 2022-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多