unittest.mock:声明方法参数的部分匹配

时间:2013-06-07 04:32:09

标签: python unit-testing mocking

Rubyist在这里编写Python。我有一些看起来像这样的代码:

result = database.Query('complicated sql with an id: %s' % id)

database.Query被模拟了,我想测试ID是否正确注入而不将整个SQL语句硬编码到我的测试中。在Ruby / RR中,我会这样做:

mock(database).query(/#{id}/)

但是我无法在unittest.mock中看到设置'选择性模拟'的方法,至少没有一些毛茸茸的side_effect逻辑。所以我尝试在断言中使用正则表达式:

with patch(database) as MockDatabase:
  instance = MockDatabase.return_value
  ...
  instance.Query.assert_called_once_with(re.compile("%s" % id))

但这也不起作用。这种方法确实有效,但它很难看:

with patch(database) as MockDatabase:
  instance = MockDatabase.return_value
  ...
  self.assertIn(id, instance.Query.call_args[0][0])

更好的想法?

4 个答案:

答案 0 :(得分:51)

import mock

class AnyStringWith(str):
    def __eq__(self, other):
        return self in other

...
result = database.Query('complicated sql with an id: %s' % id)
database.Query.assert_called_once_with(AnyStringWith(id))
...

编辑:抢先需要匹配的字符串

def arg_should_contain(x):
    def wrapper(arg):
        assert str(x) in arg, "'%s' does not contain '%s'" % (arg, x)
    return wrapper

...
database.Query = arg_should_contain(id)
result = database.Query('complicated sql with an id: %s' % id)

答案 1 :(得分:11)

您可以使用unittest.mock.ANY:)

from unittest.mock import Mock, ANY

def foo(some_string):
    print(some_string)

foo = Mock()
foo("bla")
foo.assert_called_with(ANY)

如此处所述 - https://docs.python.org/3/library/unittest.mock.html#any

答案 2 :(得分:2)

您可以使用match_equality中的PyHamcrest library来包装来自同一库的matches_regexp匹配器:

from hamcrest.library.integration import match_equality

with patch(database) as MockDatabase:
  instance = MockDatabase.return_value
  ...
  expected_arg = matches_regexp(id)
  instance.Query.assert_called_once_with(match_equality(expected_arg))

Python的unittest.mock文档中也提到了此方法:

从1.5版开始,Python测试库PyHamcrest以其相等匹配器(hamcrest.library.integration.match_equality)的形式提供了类似的功能,在这里可能有用。

如果您不想使用PyHamcrest,则上面链接的文档还显示了如何通过使用__eq__方法定义一个类来编写自定义匹配器(如falsetru答案中所建议):

class Matcher:
    def __init__(self, compare, expected):
        self.compare = compare
        self.expected = expected

    def __eq__(self, actual):
        return self.compare(self.expected, actual)

match_foo = Matcher(compare, Foo(1, 2))
mock.assert_called_with(match_foo)

您可以用自己的正则表达式匹配替换对self.compare的调用,如果找不到则返回False或使用您选择的描述性错误消息引发AssertionError

答案 3 :(得分:-1)

我总是写单元测试,以便反映“现实世界”。除了the ID gets injected in correctly之外,我真的不知道你要测试的

我不知道database.Query应该做什么,但我想它应该创建一个你可以调用或稍后传递给连接的查询对象?

您可以通过测试此方法来获取真实世界示例的最佳方式。做一些简单的事情,比如检查查询中是否出现id太容易出错。我经常看到人们想要在他们的单元测试中做出神奇的东西,这总是会导致问题。保持您的单元测试简单和静态。在你的情况下,你可以这样做:

class QueryTest(unittest.TestCase):
    def test_insert_id_simple(self):
        expected = 'a simple query with an id: 2'
        query = database.Query('a simple query with an id: %s' % 2)
        self.assertEqual(query, expected)

    def test_insert_id_complex(self):
        expected = 'some complex query with an id: 6'
        query = database.Query('some complex query with an id: %s' 6)
        self.assertEqual(query, expected)

如果database.Query直接在数据库中执行查询,您可能需要考虑使用database.querydatabase.execute之类的内容。 Query中的大写意味着你创建一个对象,如果它全部小写,它意味着你调用一个函数。这更像是一个命名惯例和我的意见,但我只是把它扔出去了。 ; - )

如果database.Query直接查询,您可以最好地修补它正在调用的方法。例如,如果它看起来像这样:

def Query(self, query):
    self.executeSQL(query)
    return query

您可以使用mock.patch来阻止单元测试进入数据库:

@mock.patch('database.executeSQL')
def test_insert_id_simple(self, mck):
    expected = 'a simple query with an id: 2'
    query = database.Query('a simple query with an id: %s' % 2)
    self.assertEqual(query, expected)

作为额外提示,请尝试使用str.format方法。 %格式可能会在将来消失。有关详细信息,请参阅this question

我也不禁觉得测试字符串格式是多余的。如果'test %s' % 'test'不起作用,那就意味着Python出了问题。只有你想测试自定义查询构建才有意义。例如插入字符串应该引用,数字不应该引用,转义特殊字符等。