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

问题描述 投票:0回答:6

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])

更好的想法?

python unit-testing mocking
6个回答
110
投票
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)

更新

使用像

callee
这样的库,你不需要实现
AnyStringWith

from callee import Contains

database.Query.assert_called_once_with(Contains(id))

https://callee.readthedocs.io/en/latest/reference/operators.html#callee.operators.Contains


36
投票

你可以使用

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


6
投票

您可以使用 PyHamcrest 库

中的 
match_equality 来包装同一库中的
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


2
投票

所选答案绝对精彩。

但是,原来的问题似乎想基于正则表达式进行匹配。我提供以下内容,如果没有 falsetru 选择的答案,我永远无法设计出以下内容:

class AnyStringWithRegex(str):
    def __init__(self, case_insensitive=True):
        self.case_insensitive = case_insensitive
    def __eq__(self, other):
        if self.case_insensitive:
            return len(re.findall(self.lower(), other.lower(), re.DOTALL)) != 0
        return len(re.findall(self, other, re.DOTALL)) != 0

毫无疑问,这个主题可以有很多变化。这根据指定的属性比较两个对象:

class AnyEquivalent():
    # compares two objects on basis of specified attributes
    def __init__(self, compared_object, *attrs):
        self.compared_object = compared_object
        self.attrs = attrs
        
    def __eq__(self, other):
        equal_objects = True
        for attr in self.attrs:
            if hasattr(other, attr):
                if getattr(self.compared_object, attr) != getattr(other, attr):
                    equal_objects = False
                    break
            else:
                equal_objects = False
                break
        return equal_objects

例如,即使文件正确,也会失败(有点令人困惑,因为错误消息显示

f
值在
str(f)
输出方面是相同的)。解释是两个文件objects是不同的:

f = open(FILENAME, 'w')
mock_run.assert_called_once_with(['pip', 'freeze'], stdout=f)

但这通过了(仅根据指定的 3 个属性的值进行显式比较):

f = open(FILENAME, 'w')
mock_run.assert_called_once_with(['pip', 'freeze'], stdout=AnyEquivalent(f, 'name', 'mode', 'encoding'))

0
投票

您可以使用模拟 call_args 来实现此目的。

def test_something(self):
  # this is your mocked method
  mocked = Mock() 

  # call method under test
  method_under_test()
  
  args, kwargs = mocked.call_args
  self.assertIn('substring for partial match', args[0])

-3
投票

我总是编写单元测试,以便它们反映“现实世界”。除了 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.query
database.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

方法。

%
格式将来可能会消失。请参阅
此问题
了解更多信息。 我也忍不住觉得测试字符串格式是多余的。如果

'test %s' % 'test'

不起作用,则意味着 Python 出现问题。仅当您想测试自定义查询构建时,这才有意义。例如插入字符串应该加引号,数字不应该,转义特殊字符等

    

© www.soinside.com 2019 - 2024. All rights reserved.