Python unittest.mock:断言方法参数的部分匹配

Python unittest.mock:断言方法参数的部分匹配,python,unit-testing,mocking,Python,Unit Testing,Mocking,Rubyist在这里编写Python。我有一些代码看起来有点像这样: result = database.Query('complicated sql with an id: %s' % id) def Query(self, query): self.executeSQL(query) return query database.Query已模拟出来,我想测试ID是否正确插入,而无需将整个SQL语句硬编码到我的测试中。在Ruby/RR中,我会这样做: mock(databa

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

result = database.Query('complicated sql with an id: %s' % id)
def Query(self, query):
    self.executeSQL(query)
    return query
database.Query
已模拟出来,我想测试ID是否正确插入,而无需将整个SQL语句硬编码到我的测试中。在Ruby/RR中,我会这样做:

mock(database).query(/#{id}/)
但我看不到一种方法来建立像unittest.mock中那样的“选择性模拟”,至少没有一些毛茸茸的
副作用
逻辑。因此,我尝试在断言中使用regexp:

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])
更好的主意

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)
更新

使用像这样的库,您不需要实现
AnyStringWith

from callee import Contains

database.Query.assert_called_once_with(Contains(id))

我总是编写单元测试,以便它们反映“真实世界”。我真的不知道你想要测试什么,除了
ID被正确地注入

我不知道
数据库.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
直接查询,您可以最好地修补它正在调用的方法。例如,如果它看起来像这样:

result = database.Query('complicated sql with an id: %s' % id)
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
方法。
%
格式可能会在将来消失。有关更多信息,请参阅


我也忍不住觉得测试字符串格式是多余的。如果
'test%s''%test'
不起作用,则表示Python有问题。只有当您想测试自定义查询构建时,它才有意义。e、 g.插入字符串应加引号,数字不应加引号,特殊字符转义等。

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

如本文所述—

您可以使用from从同一库中包装匹配器:

从hamcrest.library.integration导入匹配
使用修补程序(数据库)作为模拟数据库:
instance=MockDatabase.return\u值
...
预期的参数=匹配的参数(id)
assert_调用了一次(匹配_相等(预期_参数))
Python文档中也提到了此方法:

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

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

类匹配器:
定义初始化(自身、比较、预期):
自我比较
self.expected=预期
定义(自身、实际):
返回self.compare(self.expected,actual)
match_foo=Matcher(比较,foo(1,2))
mock.assert_调用_with(match_foo)

您可以替换对
self的调用。将此处的
与您自己的正则表达式匹配进行比较,如果未找到,则返回
False
,或者使用您选择的描述性错误消息引发
AssertionError

不错。。。但是有没有一种方法可以设置mock,以便它先发制人地需要匹配的字符串,而不必事后断言?@jpatokal添加了另一个版本。看起来您的新版本实际上是您自己的mock实现?这并不一定是错的,我只是继续对unittest.mock没有做这种事情感到惊讶…eq覆盖的解决方案很酷,但依赖于“小技巧”。我想知道是否有更好的现成解决方案。。。但到目前为止,这一个看起来是最好的。可能有点晚了,但我刚刚发布了一个库,正是这样:)因此一个无耻的插件:这是一个单元测试,而不是一个集成测试:我关心的是ID是否正确地传递到了方法调用中。(示例已经简化,实际情况中不仅仅是字符串替换。)被调用的方法在内部所做的不属于此测试级别,而且(IMHO)开始修补其他库的实现细节是一种糟糕的形式——如果我想要一个“真实世界”测试,我将编写一个贯穿整个堆栈的集成测试,并且不在中间模拟比特。它断言有什么东西被发送,但是不检查ID是否通过了。你只写了一个较长版本的<代码>断言FO。被调用的也与对象<代码>断言{“fo”:“bar”}= {“fo”:任何}真的< /代码>。