在Python单元测试中正确检查MagicMock对象

时间:2018-05-14 12:31:39

标签: python unit-testing mocking magicmock

我正在测试此代码:

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

在我的unittest中,我想声明确切地说这是通过传递的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对象是否按原样进行创建?

1 个答案:

答案 0 :(得分:1)

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

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

  • x.a + x.b调用m.a.__add__,传入m.b
  • m.a.__add__().c()被调用,因此如果需要,我们可以测试它被调用。
  • 只需将c()的结果设置为round()的数字即可完成。从函数中获取正确的round(number)结果意味着.c()被调用。

将数字传递到round()就足够了,因为您没有测试round()函数。您可以依靠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类型,那么让你的模拟产生这些类型。例如。如果某个调用需要产生一个字符串,请让你的mock返回一个测试字符串,特别是当你将东西传递给其他标准库API时。
  • 模拟是单身人士。您不需要从给定的模拟器返回来测试它们是否具有正确的父级,因为您可以通过遍历父属性来访问同一个对象,然后使用is。例如。如果函数在某处返回模拟对象,则可以通过测试assertIs(mock_object.some.access.return_value.path, returned_object)断言 right 模拟对象已返回。
  • 当调用模拟时,会记录该事实。您可以使用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'>)]