在Python单元测试中打补丁的首选方式

30

我需要在进行单元测试时,先用模拟方法替换掉我的三个方法(_send_reply, _reset_watchdog_handle_set_watchdog),然后再测试我的第四个方法(_handle_command)的调用。

从查看mock包的文档中,有几种方法可供选择:

使用patch.multiple作为装饰器

@patch.multiple(MBG120Simulator,
                _send_reply=DEFAULT,
                _reset_watchdog=DEFAULT,
                _handle_set_watchdog=DEFAULT,
                autospec=True)
def test_handle_command_too_short_v1(self,
                                     _send_reply,
                                     _reset_watchdog,
                                     _handle_set_watchdog):
    simulator = MBG120Simulator()
    simulator._handle_command('XA99')
    _send_reply.assert_called_once_with(simulator, 'X?')
    self.assertFalse(_reset_watchdog.called)
    self.assertFalse(_handle_set_watchdog.called)
    simulator.stop()

使用patch.multiple作为上下文管理器

def test_handle_command_too_short_v2(self):
    simulator = MBG120Simulator()

    with patch.multiple(simulator,
                        _send_reply=DEFAULT,
                        _reset_watchdog=DEFAULT,
                        _handle_set_watchdog=DEFAULT,
                        autospec=True) as mocks:
        simulator._handle_command('XA99')
        mocks['_send_reply'].assert_called_once_with('X?')
        self.assertFalse(mocks['_reset_watchdog'].called)
        self.assertFalse(mocks['_handle_set_watchdog'].called)
        simulator.stop()

使用多个patch.object修饰器

@patch.object(MBG120Simulator, '_send_reply', autospec=True)
@patch.object(MBG120Simulator, '_reset_watchdog', autospec=True)
@patch.object(MBG120Simulator, '_handle_set_watchdog', autospec=True)
def test_handle_command_too_short_v3(self,
                                     _handle_set_watchdog_mock,
                                     _reset_watchdog_mock,
                                     _send_reply_mock):
    simulator = MBG120Simulator()
    simulator._handle_command('XA99')
    _send_reply_mock.assert_called_once_with(simulator, 'X?')
    self.assertFalse(_reset_watchdog_mock.called)
    self.assertFalse(_handle_set_watchdog_mock.called)
    simulator.stop()

使用create_autospec手动替换方法

def test_handle_command_too_short_v4(self):
    simulator = MBG120Simulator()

    # Mock some methods.
    simulator._send_reply = create_autospec(simulator._send_reply)
    simulator._reset_watchdog = create_autospec(simulator._reset_watchdog)
    simulator._handle_set_watchdog = create_autospec(simulator._handle_set_watchdog)

    # Exercise.
    simulator._handle_command('XA99')

    # Check.
    simulator._send_reply.assert_called_once_with('X?')
    self.assertFalse(simulator._reset_watchdog.called)
    self.assertFalse(simulator._handle_set_watchdog.called)

个人认为最后一种方法最清晰易读,如果模拟的方法数量增加,也不会导致可怕的长行。它还避免了将simulator作为第一个(self)参数传递给assert_called_once_with

但我并不觉得它们中的任何一种特别好。特别是多个patch.object方法的方法,需要仔细匹配嵌套装饰器的参数顺序。

是否有我错过的方法或使其更易读的方法?当您需要在实例/测试类上修补多个方法时,您会怎么做?

1个回答

20

你没有错过任何与你提出的不同的东西。

关于可读性,我的喜好是使用装饰器方式,因为它可以将测试体中的嘲弄内容删除...但这只是个人喜好。

你是对的:如果你通过autospec=True修补方法的静态实例,则必须在assert_called_*家族检查方法中使用self。但你的情况只是一个小类,因为你确切地知道需要修补哪个对象,并且不需要其他上下文来进行修补,只需在测试方法中使用它即可。

你只需要修补你的对象并在所有测试中使用它:通常在测试中,在进行调用之前无法获得要修补的实例,在这种情况下不能使用create_autospec,而只能修补方法的静态实例。

如果你对传递给assert_called_*方法的实例感到困扰,请考虑使用ANY来打破依赖关系。最后,我编写了数百个类似的测试,并且从未遇到过关于参数顺序的问题。

我在你的测试中的标准方法是

from unittest.mock import patch

@patch('mbgmodule.MBG120Simulator._send_reply', autospec=True)
@patch('mbgmodule.MBG120Simulator._reset_watchdog', autospec=True)
@patch('mbgmodule.MBG120Simulator._handle_set_watchdog', autospec=True)
def test_handle_command_too_short(self,mock_handle_set_watchdog,
                                          mock_reset_watchdog,
                                          mock_send_reply):
    simulator = MBG120Simulator()
    simulator._handle_command('XA99')
    # You can use ANY instead simulator if you don't know it
    mock_send_reply.assert_called_once_with(simulator, 'X?')
    self.assertFalse(mock_reset_watchdog.called)
    self.assertFalse(mock_handle_set_watchdog_mock.called)
    simulator.stop()
  • 补丁是在测试方法代码之外
  • 每个模拟都以mock_前缀开头
  • 我更喜欢使用简单的patch调用和绝对路径:这样可以清楚地了解您正在做什么

最后:也许创建simulator并停止它是setUp()tearDown()的责任,而测试应该考虑仅修补一些方法并进行检查。

我希望这个答案有用,但问题没有唯一有效的答案,因为可读性不是绝对的概念,而是取决于读者。此外,即使标题涉及一般情况,问题示例也涉及特定类别的问题,您需要修补要测试的对象的方法。

[编辑]

我思考了一段时间关于这个问题,我发现了什么困扰我:您正在尝试测试和感知私有方法。当发生这种情况时,您应该首先问的是为什么?很有可能的答案是因为这些方法应该是私有协作者的公共方法(这不是我的话)。

在这种新的情况下,您应该感知私有协作者,并且不能仅更改自己的对象。您需要做的是修补其他某些类的静态实例。


非常感谢您的详细回答。按顺序:1)关于我的情况很特殊(我可以访问实例等),您提出了一个很好的观点。 2)感谢您提供ANY的提示。 3)是的,也许我在使用装饰器时夸大了参数顺序的脆弱性。 4)问题:因此@patch('package.module.Class.some_method', autospec=True)等同于@patch.object(package.module.Class, 'some_method', autospec=True)吗?如果是这样,我也更喜欢您的方法。 - estan
抱歉,格式很糟糕。我真的不太喜欢SO的评论功能。无论如何,我接受你的答案,因为它非常好。 - estan
@estan... 我曾经在私有数据上进行测试和感知,但是自从我停止这样做后,我发现我的设计变得更好了,并且花费了一些努力来暴露一些属性并创建一个新类,它只有一个责任,可以产生非常整洁的代码。 - Michele d'Amico
是的,我同意,如果有意义的话,功能应该被拆分。在这种情况下,它是一个小类,几乎只有一个单一的职责(模拟具有串行协议、X射线管的简单硬件),我认为如果我开始委派给另一个类,这将会令人困惑。这些私有命令处理程序方法没有任何机会对其他人有用。 - estan
关于 setUp()tearDown() 这只是我的个人偏好。我喜欢让每个测试方法都完全独立,这对于那些偶尔只读一遍的读者来说很有帮助,他们不需要导航到 setUp() 方法才能完全理解正在发生的事情。 - estan
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接