在Python中添加Mock对象

时间:2018-05-11 13:58:37

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

我正在测试的代码是这样的:

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'。我不理解,因为我在那里创建的模拟应该接受我在其中调用了c(),对吗?

有没有办法可以实现我的目标?如何创建一个能够添加到另一个Mock的模拟?

或者是否有其他标准方法对我的代码进行单元测试?

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

2 个答案:

答案 0 :(得分:1)

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

  

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

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

这使您可以访问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()

同样,这是我们可以断言的。请注意,如果你重复这个电话,你会发现嘲笑是单身;当访问属性或调用mock时,会创建更多相同类型的模拟并存储,稍后您可以使用这些存储的对象断言正确的对象已被分发;您可以使用Mock.return_value attribute

来达到通话结果
>>> (m.a + m.b).c()
<MagicMock name='mock.a.__add__().c()' id='4500162544'>

现在,转到>>> 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__() method。不幸的是,这不在支持的方法列表中:

round()

这可能是一种疏忽,因为其他数字方法(例如>>> round(mock.MagicMock()) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: type MagicMock doesn't define __round__ method __trunc__ )都包含在中。我提交了a bug report to request it to be added。您可以使用以下命令手动将其添加到__ceil__支持的方法列表中:

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可以帮助您测试 调用,所有调用,特定顺序的调用等。还有一些属性,例如{{ 3}},.called.call_count。请检查一下。

    断言发生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实例返回值,请遍历到预期的模拟对象,并使用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上的错误。

答案 1 :(得分:0)

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

正常的Mock物体准备记录他们经历的许多治疗,但不是全部。 E. g。它们将在被调用时,在查询属性时以及更多内容时进行记录。但是,如果他们是e,他们不会记录(或接受)。 G。相互补充。添加被假定为&#34;魔术&#34;操作,使用&#34;魔术方法&#34; (__add__)对象和Mock不支持它们。

对于这些,还有另一个名为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的名称(以及谁知道还有什么)。