【问题标题】:Adding Mock objects in Python在 Python 中添加 Mock 对象
【发布时间】:2018-10-21 23:18:40
【问题描述】:

我的测试代码是这样的:

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

我想通过将 Mock 对象作为 x 传递来测试它。我试着这样做:

import unittest
import unittest.mock

class Test_X(unittest.TestCase):
  def test_x(self):
    m = unittest.mock.Mock()
    to_be_tested(m)
    # now check if the proper call has taken place on m

x.a 的调用和对x.b 的调用按预期工作。他们提供了新的模拟对象,可以询问它们是如何创建的(例如通过q._mock_parentq._mock_new_name),所以这一步工作得很好。

但随后应该进行添加,这只会引发错误 (TypeError: unsupported operand type(s) for +: 'Mock' and 'Mock')。我希望这也能返回一个模拟对象,以便可以调用 .c() 并(再次)返回一个模拟对象。

在调用被测代码之前,我也考虑过m.__add__ = lambda a, b: unittest.mock.Mock(a, b),但这无济于事,因为要添加的不是我原来的 Mock,而是一个新创建的。

我还尝试了(已经很麻烦的)m.a.__add__ = lambda a, b: unittest.mock.Mock(a, b)。但这(令我惊讶)导致在调用被测代码时引发AttributeError: Mock object has no attribute 'c'。我不明白,因为我在那里创建的 Mock 应该接受我在其中调用 c(),对吗?

有什么方法可以实现我想要的吗?如何创建能够添加到另一个 Mock 的 Mock?

或者有像我这样的单元测试代码的另一种标准方法吗?

编辑:我对提供专门的代码准备为预期调用传递的模拟不感兴趣。我只想在调用后通过检查传递和返回的模拟对象来检查一切是否按预期进行。我认为这种方式应该是可能的,并且在这种情况下(以及其他类似的复杂情况)我可以使用它。

【问题讨论】:

  • 投反对票的人:请说明您对我的问题不满意的地方。
  • 简单的没有足够的信息可用。没有关于class X 的信息,2 个datetime.datetime 对象自然不能加在一起。如果您只在评论中添加它们,我认为我现在无法投票,我认为您必须更新原始帖子
  • @Gang 原来使用了两个datetime 对象的差异,这是可能的,应该返回一个datetime.timedelta()。我只是想提供一个minimal reproducible example 而不提供详细信息。我知道,考虑到所有细节,您可能已经能够提出一个专门的版本,但是使用我想要理解和使用的机制(传递 Mock 对象并在之后签入),这将不是一个合适的解决方案)。我在我的 Q 中添加了一个 EDIT 部分,以使任何反对者能够撤销他们的决定(以防万一)。
  • 请注意,unittest.mock 不提供 __round__ 模拟实现可能是有充分理由的:文档明确指出,此类方法应返回 Integral(整数)类型,模拟对象不是。因此,您通常需要提供自己的 __round__ 实现。
  • 也就是说,由于所有其他数字舍入方法都存在(trunc、ceil 和 floor),我已提交 bugs.python.org/issue33516 以在核心库中解决此问题。

标签: python-3.x unit-testing mocking


【解决方案1】:

添加对象需要这些对象至少实现__add__,这是一种特殊的方法,被Mock称为魔术方法,请参阅文档中的Mocking Magic Methods section

由于查找魔术方法与普通方法不同,因此已专门实施了此支持。这意味着只支持特定的魔术方法。支持的列表几乎包括所有这些。如果您需要任何遗漏,请告诉我们。

访问mock 支持的魔术方法的最简单方法是创建MagicMock class 的实例,它为这些方法提供默认实现(默认情况下每个都返回一个现在的MagicMock 实例) .

这使您可以访问x.a + x.b 呼叫:

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

已记录对m.a.__add__() 的调用,参数为m.b;这是我们现在可以在测试中断言的东西!

接下来,使用相同的 m.a.__add__() 模拟来提供 .c() 模拟:

>>> (m.a + m.b).c()
<MagicMock name='mock.a.__add__().c()' id='4500162544'>

同样,这是我们可以断言的。请注意,如果您重复此调用,您会发现模拟是单例;在访问属性或调用 mock 时,会创建更多相同类型的 mock 并存储,您可以稍后使用这些存储的对象来断言正确的对象已被分发;您可以使用Mock.return_value attribute 获得调用结果:

>>> m.a.__add__.return_value.c.return_value
<MagicMock name='mock.a.__add__().c()' id='4500162544'>
>>> (m.a + m.b).c() is m.a.__add__.return_value.c.return_value
True

现在,转到round()round() 也调用了一个神奇的方法,__round__() method。不幸的是,这不在支持的方法列表中:

>>> round(mock.MagicMock())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type MagicMock doesn't define __round__ method

这可能是一个疏忽,因为包括其他数字方法,例如 __trunc____ceil__ 。我提交了a bug report to request it to be added。您可以手动将其添加到 MagicMock 支持的方法列表中:

mock._magics.add('__round__')   # set of magic methods MagicMock supports

_magics 是一个集合;当 __round__ 已经存在于该集合中时添加它是无害的,因此上述内容是面向未来的。另一种解决方法是模拟round() 内置函数,使用mock.patch() 在被测函数所在的模块中设置一个新的round 全局变量。

接下来,在测试时,您有 3 个选项:

  • 通过设置调用的返回值来驱动测试,包括模拟以外的类型。例如,您可以将模拟设置为为 .c() 调用返回一个浮点值,这样您就可以断言您得到正确的舍入结果:

        >>> m.a.__add__.return_value.c.return_value = 42.12   # (m.a + ??).c() returns 42.12
        >>> round((m.a + m.b).c()) == 42
        True
    
  • 断言已发生特定调用。有一个whole series of assert_call* methods 可以帮助您测试a 调用、所有 调用、特定顺序的调用等。还有.called 等属性, .call_countmock_calls。请检查这些。

    断言m.a + m.b 发生意味着断言m.a.__add__ 是用m.b 作为参数调用的:

    >>> m = mock.MagicMock()
    >>> m.a + m.b
    <MagicMock name='mock.a.__add__()' id='4500337776'>
    >>> m.a.__add__.assert_called_with(m.b)  # returns None, so success
    
  • 如果要测试一个Mock实例返回值,遍历预期的mock对象,使用is进行身份测试:

    >>> mock._magics.add('__round__')
    >>> m = mock.MagicMock()
    >>> r = round((m.a + m.b).c())
    >>> mock_c_result = m.a.__add__.return_value.c.return_value
    >>> r is mock_c_result.__round__.return_value
    True
    

永远不需要从模拟结果返回给父母等。只需遍历其他方式。

__add__ 的 lambda 不起作用的原因是您创建了一个带有参数的 Mock() 实例。前两个参数是 specside_effect 参数。 spec 参数限制了模拟支持的属性,并且由于您将a 作为模拟对象规范传入并且a 对象没有属性c,因此您会在c 上收到属性错误。

【讨论】:

  • 不从结果返回而是从最初传递的Mock 向前遍历的想法很有趣。但是重构步骤可能会意外取出return 语句,因此CUT 只返回None。调用可能仍然全部发生,因此这种测试方式不会注意到错误。不,我认为通过结果如何产生来检验它是合理的。 MagicMock 对象似乎是一种完美的方法,它们似乎缺乏一些逻辑来使这在所有情况下都成为可能,并且(最重要的是)简单易读。
  • @Alfe:我没有说不测试返回值。如果返回None,那显然不等于42mock_c_result.__round__.return_value 是同一个对象。您结合了不同的技术(测试返回值,并进行了断言调用)。
  • 是的,当然。但是我想测试以正确方式创建的返回值,不仅是对于我的示例性输入值,返回值是正确的。我知道测试的想法通常是通过(大量)示例来完成,然后依靠它们涵盖所有情况(通过测试覆盖率分析检查)。在许多语言中,这也是唯一可行的方法。但在 Python 中似乎可以做更多的分析工作,所以我还想分析返回值的创建方式(在某些情况下)。
【解决方案2】:

我自己找到了一个解决方案,但它并不是很漂亮。忍受我。

普通的Mock对象准备记录他们经历的很多治疗,但不是全部。例如。他们会记录什么时候被调用,什么时候有一个属性被查询,还有一些事情。但是,如果他们是 e,他们将不会记录(或接受)。 G。彼此相加。添加被假定为“魔术”操作,使用对象的“魔术方法”(__add__),Mocks 不支持它们。

对于这些,还有另一个名为MagicMock 的类。 MagicMock 对象支持魔术方法,因此添加它们对它们有效。结果将是另一个MagicMock 对象,可以询问它是如何创建的(通过添加另外两个MagicMock 对象)。

不幸的是,在当前版本 (3.6.5) 中,魔术方法 __round__(在调用 round(o) 时调用)尚未包含。我的猜测是他们只是忘记在__trunc____floor____ceil__ 等其他魔术方法中列出它。当我将它添加到源代码中时,我也可以正确测试我的测试代码,包括round()打电话。

但是,修补已安装的 Python 模块当然不是解决问题的方法。由于这是当前实现中的一个缺陷,我希望将来会修复它,因此我目前的解决方案是仅在导入 mock 模块后更改其内部数据结构。

我的测试现在看起来是这样的:

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

import unittest
import unittest.mock

# patch mock module's internal data structures to support round():
unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')

class Test_X(unittest.TestCase):
  def test_x(self):
    m = unittest.mock.MagicMock()
    r = to_be_tested(m)
    # now for the tests:
    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)

Test_X().test_x()

遗憾的是,像 self.assertEqual(r, round((m.a + m.b).c())) 这样的简单测试还不够,因为它不会检查属性名称 b(谁知道还有什么)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-23
    • 1970-01-01
    • 1970-01-01
    • 2011-04-28
    • 2019-04-02
    • 2015-10-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多