首页 > 解决方案 > 在 Python 单元测试中正确检查 MagicMock 对象

问题描述

我正在测试这段代码:

def to_be_tested(x):
  return round((x.a + x.b).c())

在我的单元测试中,我想断言这正是通过传递x并返回结果来完成的,所以我将一个MagicMock对象传递为x

class Test_X(unittest.TestCase):
  def test_x(self):
    m = unittest.mock.MagicMock()
    r = to_be_tested(m)

然后我检查结果是否符合我的预期:

    self.assertEqual(r._mock_new_name, '()')  # created by calling
    round_call = r._mock_new_parent
    self.assertEqual(round_call._mock_new_name, '__round__')
    c_result = round_call._mock_new_parent
    self.assertEqual(c_result._mock_new_name, '()')  # created by calling
    c_call = c_result._mock_new_parent
    self.assertEqual(c_call._mock_new_name, 'c')
    add_result = c_call._mock_new_parent
    self.assertEqual(add_result._mock_new_name, '()')  # created by calling
    add_call = add_result._mock_new_parent
    self.assertEqual(add_call._mock_new_name, '__add__')
    a_attribute = add_call._mock_new_parent
    b_attribute = add_call.call_args[0][0]
    self.assertEqual(a_attribute._mock_new_name, 'a')
    self.assertEqual(b_attribute._mock_new_name, 'b')
    self.assertIs(a_attribute._mock_new_parent, m)
    self.assertIs(b_attribute._mock_new_parent, m)

导入后unittest.mock,我需要修补mock模块的内部结构,以便能够正确地对round()函数进行魔术模拟(有关详细信息,请参阅https://stackoverflow.com/a/50329607/1281485):

unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')

所以,现在,正如我所说,这行得通。但我觉得它非常难以阅读。此外,我需要花很多时间才能找到诸如此类的东西_mock_new_parent。下划线还表示这是一个“私有”属性,不应该使用。文档没有提到它。它也没有提到实现我尝试的另一种方法。

是否有更好的方法来测试返回MagicMock的对象是否按照应有的方式创建?

标签: pythonunit-testingmockingmagicmock

解决方案


你太过分了。您正在测试实现,而不是结果。此外,您正在进入不需要接触的模拟实现的内部。

测试您是否获得了正确的结果,并测试该结果是否基于您要使用的输入。您可以设置模拟,以便round()将实际数值传递给四舍五入:

  • x.a + x.b导致调用m.a.__add__,传入m.b
  • m.a.__add__().c()被调用,所以我们可以测试它是否在需要时被调用。
  • 只需将结果设置c()为一个数字round()即可四舍五入。round(number)调用了从函数获取正确结果的方法.c()

在这里传递一个数字round()就足够了,因为您没有测试round()function。您可以依靠 Python 维护人员来测试该功能,专注于测试您自己的代码。

这是我要测试的:

m = unittest.mock.MagicMock()

# set a return value for (x.a + *something*).c()
mock_c = m.a.__add__.return_value.c
mock_c.return_value = 42.4

r = to_be_tested(m)

mock_c.assert_called_once()
self.assertEqual(r, 42)

如果您必须断言m.a + m.b发生了,那么您可以添加

m.a.__add__.assert_called_once(m.b)

但是mock_c调用断言传递已经证明至少发生了一个(m.a + <whatever>)表达式并且c在结果上被访问。

如果您必须验证round()在实际模拟实例上使用的内容,则必须坚持修补MagicMock类以包含__round__为特殊方法并删除mock_c.return_value分配,之后您可以断言返回值是正确的对象

# assert that the result of the `.c()` call has been passed to the
# round() function (which returns the result of `.__round__()`).
self.assertIs(r, mock_c.return_value.__round__.return_value)

一些进一步的说明:

  • 试图让一切都成为模拟对象是没有意义的。如果被测代码应该适用于标准 Python 类型,只需让您的模拟生成这些类型。例如,如果某个调用预计会产生一个字符串,则让您的模拟返回一个测试字符串,尤其是当您随后将内容传递给其他标准库 API 时。
  • 模拟是单例。您无需从给定的模拟中回过头来测试它们是否具有正确的父对象,因为您可以通过遍历父属性然后使用is. 例如,如果一个函数在某处返回一个模拟对象,您可以断言正确的模拟对象是由 testing 返回的assertIs(mock_object.some.access.return_value.path, returned_object)
  • 当调用模拟时,会记录该事实。assert_called*您可以使用方法、.called和属性对此进行断言,并使用属性.call_count遍历调用的结果.return_value
  • 如有疑问,请检查.mock_calls属性以查看被测代码已访问的内容。或者在交互式会话中这样做。例如,通过以下方式可以更轻松地查看m.a + m.b快速测试中的作用:

    >>> from unittest import mock
    >>> m = mock.MagicMock()
    >>> m.a + m.b
    <MagicMock name='mock.a.__add__()' id='4495452648'>
    >>> m.mock_calls
    [call.a.__add__(<MagicMock name='mock.b' id='4495427568'>)]
    

推荐阅读