61

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

5 回答 5

94
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

于 2013-06-07T04:57:11.107 回答
23

你可以使用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

于 2017-02-20T16:21:27.350 回答
4

您可以使用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))

unittest.mockPython的文档中也提到了这种方法:

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

如果您不想使用 PyHamcrest,上面链接的文档还显示了如何通过定义具有方法的类来编写自定义匹配器(如s 回答__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使用您选择的描述性错误消息引发 an。

于 2020-07-15T10:49:43.423 回答
1

选择的答案绝对精彩。

但是,最初的问题似乎想在正则表达式的基础上进行匹配。我提供以下内容,如果没有 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))。解释是两个文件对象是不同的:

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'))
于 2021-12-12T22:10:36.500 回答
-2

我总是编写单元测试,以便它们反映“现实世界”。我真的不知道你想测试什么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.queryor的东西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 有问题。仅当您想测试自定义查询构建时才有意义。例如插入字符串应该被引用,数字不应该,转义特殊字符等。

于 2013-06-11T08:43:49.947 回答