【问题标题】:unittest.mock: asserting partial match for method argumentunittest.mock:断言方法参数的部分匹配
【发布时间】:2013-06-03 07:09:14
【问题描述】:

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


    【解决方案1】:

    The chosen answer 太棒了。

    但是,最初的问题似乎想根据正则表达式进行匹配。我提供以下内容,如果没有 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())) != 0
            return len(re.findall(self, other)) != 0
    

    【讨论】:

      【解决方案2】:

      您可以使用来自PyHamcrest librarymatch_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__ 方法定义一个类来编写自定义匹配器(如falsetrus 答案中所建议的那样):

      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】:
        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

        【讨论】:

        • 不错...但是有没有办法设置模拟,所以它预先需要一个匹配的字符串,而不是事后断言?
        • @jpatokal,添加了另一个版本。
        • 看起来您的新版本实际上是您自己的模拟实现?这不一定是错误的,我只是继续对 unittest.mock 不做这种事情感到惊讶......
        • 覆盖 eq 的解决方案很酷,但依赖于“小技巧”。我想知道是否有更好的开箱即用解决方案......但到目前为止,这个看起来是最好的。
        • 聚会可能有点晚了,但我刚刚发布了一个正是这样的库 :) 因此一个无耻的插件:github.com/Xion/callee
        【解决方案4】:

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

        【讨论】:

        • 断言发送了一些东西,但不检查 ID 是否通过。
        • 你只写了一个较长的assert foo.called
        • 也适用于对象assert {"foo": "bar"} == {"foo": ANY} # True
        【解决方案5】:

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

        【讨论】:

        • 这是一个单元测试,而不是集成测试:我关心 ID 是否已正确传递到方法调用中。 (示例已被简化,实际情况中不仅仅是字符串替换。)被调用的方法在内部执行的操作不属于此级别的测试,并且(恕我直言)开始修补实现细节是一种不好的形式其他库——如果我想要一个“真实世界”的测试,我会编写一个集成测试,它会遍历整个堆栈并且不会模拟中间的位。
        猜你喜欢
        • 2022-07-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-22
        • 1970-01-01
        • 2021-02-13
        相关资源
        最近更新 更多