1

TL;博士

我可以unittest.TestCase从模拟类的方法中访问当前实例而不显式传递该实例吗?TestCase如果不是,从那里访问(实例方法)的断言辅助函数的正确方法是什么?(有合适的​​方法吗?)

Szenario:使用我自己的基于方法MagicMock的断言方法进行增强TestCase

我想测试一个函数()是否正确handle_multiple_foobars()使用了另一个函数(handle_one_foobar()),所以我在嘲笑handle_one_foobar()。这里的“正确”意味着handle_multiple_foobars()应单独调用handle_one_foobar()其每个参数。(handle_one_foobar()每个handle_multiple_foobars()参数一个调用。)我不关心调用的顺序。

检查是否已对模拟进行了所有预期的调用

因此,我从这个开始:

import unittest
from unittest import TestCase
from unittest.mock import patch, call

def handle_one_foobar(foobar):
    raise NotImplementedError()

def handle_multiple_foobars(foobars):
    for foobar in reversed(foobars):
        handle_one_foobar(foobar)


class FoobarHandlingTest(TestCase):
    @patch('__main__.handle_one_foobar')
    def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar(
            self, handle_one_foobars_mock):
        foobars = ['foo', 'bar', 'foo', 'baz']

        handle_multiple_foobars(foobars)

        expected_calls = [call(fb) for fb in foobars]
        handle_one_foobars_mock.assert_has_calls(
                expected_calls,
                any_order=True)

if __name__ == '__main__':
    unittest.main()

检查是否对模拟进行了预期的调用

但是,如果对模拟函数有更多调用,这也会通过,例如

def handle_multiple_foobars(foobars):
    handle_one_foobar('begin')
    for foobar in reversed(foobars):
        handle_one_foobar(foobar)
    handle_one_foobar('end')

我不想要那个。

我可以很容易地编写一个额外的断言(或一个额外的测试)来检查调用的总数。但我希望这被视为一个单一的条件来测试。所以我构建了一个不同的断言:

class FoobarHandlingTest(TestCase):
    @patch('__main__.handle_one_foobar')
    def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar(
            self, handle_one_foobar_mock):
        foobars = ['foo', 'bar', 'foo', 'baz']

        handle_multiple_foobars(foobars)

        expected_calls = [call(fb) for fb in foobars]
        self.assertCountEqual(
                handle_one_foobar_mock.mock_calls,
                expected_calls)

这将很好地捕获额外的调用:

F
======================================================================
FAIL: test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar (__main__.FoobarHandlingTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.4/unittest/mock.py", line 1136, in patched
    return func(*args, **keywargs)
  File "convenientmock.py", line 23, in test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar
    expected_calls)
AssertionError: Element counts were not equal:
First has 1, Second has 0:  call('begin')
First has 1, Second has 0:  call('end')

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

将新断言移动到模拟的辅助方法中

但就我的口味而言,这里所断言的内容还不够明显。因此,我决定将断言提取到一个新函数中。因为这个函数的作用和 非常相似assert_has_calls(),我觉得应该是mock类的一个方法。扩展并不难MagicMock,我们甚至可以使提取的方法更通用,以允许指定调用顺序是否重要:

from unittest.mock import MagicMock

class MyMock(MagicMock):
    def assert_has_exactly_calls(_mock_self, calls, any_order=False):
        tc = TestCase()
        asserter = tc.assertCountEqual if any_order else tc.assertEqual
        asserter(_mock_self.mock_calls, list(calls))

@patch当我将测试方法装饰更改为时,将使用此类而不是unittest.mock.MagicMock创建模拟

    @patch('__main__.handle_one_foobar', new_callable=MyMock)
    def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar( # ...

然后我可以把我的断言写成

        handle_one_foobar_mock.assert_has_exactly_calls(
                expected_calls,
                any_order=True)

...和丑陋的

但是您可能已经注意到一些非常丑陋的事情:为了能够使用TestCase实例方法assertCountEqual()assertEqual(),我创建了一个虚拟TestCase实例,它不是运行测试的真实FoobarHandlingTest实例。

我怎样才能避免这种情况?

显然,我可以将测试传递给self断言方法,但这会导致非常不直观的签名。(为什么我必须对我的测试用例进行断言?)

4

0 回答 0