4

要创建给定函数的单元测试,我需要修补''.join(...).

我已经尝试了很多方法来做到这一点(使用模拟库),但即使我有一些使用该库创建单元测试的经验,我也无法让它工作。

出现的第一个问题是它str是一个内置类,因此它不能被模拟。William John Bert的一篇文章展示了如何处理这个问题(datetime.date在他的案例中)。图书馆官方文档的“部分模拟”部分也有一个可能的解决方案。

第二个问题是str没有真正直接使用。而是调用join文字方法。''那么,补丁的路径应该是什么?

这些选项都不起作用:

  • patch('__builtin__.str', 'join')
  • patch('string.join')
  • patch('__builtin__.str', FakeStr)(哪里FakeStr是 的子类str

任何帮助将不胜感激。

4

5 回答 5

4

您不能,因为无法在内置类上设置属性:

>>> str.join = lambda x: None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'

并且您无法修补str,因为''.join使用文字,因此无论您如何尝试替换in ,解释器都会始终创建.strstr__builtin__

如果您阅读生成的字节码,您可以看到这一点:

>>> import dis
>>> def test():
...     ''.join([1,2,3])
... 
>>> dis.dis(test)
  2           0 LOAD_CONST               1 ('')
              3 LOAD_ATTR                0 (join)
              6 LOAD_CONST               2 (1)
              9 LOAD_CONST               3 (2)
             12 LOAD_CONST               4 (3)
             15 BUILD_LIST               3
             18 CALL_FUNCTION            1
             21 POP_TOP             
             22 LOAD_CONST               0 (None)
             25 RETURN_VALUE

字节码是在编译时生成的,如您所见,无论您在运行时如何更改值,首先LOAD_CONST加载''的是 a 。strstr

可以做的是使用可以模拟的包装函数,或者避免使用文字。例如,使用str()而不是''允许您使用具有实现您想要mock的方法str的子类的类join(即使这可能会影响太多代码并且根据您使用的模块可能不可行)。

于 2013-02-28T22:21:42.137 回答
3

如果您感到非常幸运,您可以检查并修补代码对象中的字符串常量:

def patch_strings(fun, cls):
    new_consts = tuple(
                  cls(c) if type(c) is str else c
                  for c in fun.func_code.co_consts)

    code = type(fun.func_code)

    fun.func_code = code(
           fun.func_code.co_argcount,
           fun.func_code.co_nlocals, 
           fun.func_code.co_stacksize,
           fun.func_code.co_flags,
           fun.func_code.co_code,
           new_consts,
           fun.func_code.co_names,
           fun.func_code.co_varnames,
           fun.func_code.co_filename,
           fun.func_code.co_name,
           fun.func_code.co_firstlineno,
           fun.func_code.co_lnotab,
           fun.func_code.co_freevars,
           fun.func_code.co_cellvars)

def a():
    return ''.join(['a', 'b'])

class mystr(str):
    def join(self, s):
        print 'join called!'
        return super(mystr, self).join(s)

patch_strings(a, mystr)
print a()      # prints "join called!\nab"

Python3版本:

def patch_strings(fun, cls):
    new_consts = tuple(
                   cls(c) if type(c) is str else c
                   for c in fun.__code__.co_consts)

    code = type(fun.__code__)

    fun.__code__ = code(
           fun.__code__.co_argcount,
           fun.__code__.co_kwonlyargcount,
           fun.__code__.co_nlocals, 
           fun.__code__.co_stacksize,
           fun.__code__.co_flags,
           fun.__code__.co_code,
           new_consts,
           fun.__code__.co_names,
           fun.__code__.co_varnames,
           fun.__code__.co_filename,
           fun.__code__.co_name,
           fun.__code__.co_firstlineno,
           fun.__code__.co_lnotab,
           fun.__code__.co_freevars,
           fun.__code__.co_cellvars)
于 2013-02-28T22:42:58.840 回答
0

确实没有办法使用字符串文字来执行此操作,因为它们始终使用内置str类,正如您所发现的那样,它不能以这种方式修补。

当然,您可以编写一个函数join(seq, sep='')来代替''.join()和修补它,或者您总是使用一个str子类Separator来显式地构造将用于join操作的字符串(例如Separator('').join(....))。这些变通办法有点难看,但您不能以其他方式修补该方法。

于 2013-02-28T22:26:55.440 回答
0

在这里,我正在修补我正在测试的模块中的变量。我不喜欢这个想法,因为我正在更改我的代码以适应测试,但它确实有效。

测试.py

import mock

from main import func


@mock.patch('main.patched_str')
def test(patched_str):
    patched_str.join.return_value = "hello"

    result = func('1', '2')

    assert patched_str.join.called_with('1', '2')
    assert result == "hello" 


if __name__ == '__main__':
    test()

主文件

patched_str = ''

def func(*args):
    return patched_str.join(args)
于 2013-02-28T23:08:55.013 回答
0

我的解决方案有点棘手,但它适用于大多数情况。它不使用模拟库,顺便说一句。我的解决方案的优点是您可以继续使用''.join而无需进行丑陋的修改。

当我不得不在 Python3.2 中运行为 Python3.3 编写的代码时,我发现了这种方法(它替换str(...).casefoldstr(...).lower

假设你有这个模块:

# my_module.py

def my_func():
    """Print some joined text"""
    print('_'.join(str(n) for n in range(5)))

有一个用于测试它的单元测试示例。请注意,它是为 Python 2.7 编写的,但可以很容易地针对 Python 3 进行修改(请参阅注释):

import re
from imp import reload  # for Python 3

import my_module


class CustomJoinTets(unittest.TestCase):
    """Test case using custom str(...).join method"""
    def setUp(self):
        """Replace the join method with a custom function"""
        with open(my_module.__file__.replace('.pyc', '.py')) as f:
            # Replace `separator.join(` with `custom_join(separator)(`
            contents = re.sub(r"""(?P<q>["'])(?P<sep>.*?)(?P=q)[.]join\(""",
                              r"custom_join(\g<q>\g<sep>\g<q>)(",
                              f.read())

        # Replace the code in the module
        # For Python 3 do `exec(contents, my_module.__dict__)`
        exec contents in my_module.__dict__

        # Create `custom_join` object in the module
        my_module.custom_join = self._custom_join

    def tearDown(self):
        """Reload the module"""
        reload(my_module)

    def _custom_join(self, separator):
        """A factory for a custom join"""
        separator = '+{}+'.format(separator)
        return separator.join

    def test_smoke(self):
        """Do something"""
        my_module.my_func()

if __name__ == '__main__':
    unittest.main()

如果你真的想要mock库,你可以让_custom_join方法返回 MagicMock 对象:

    def _custom_join(self, separator):
        """A factory for a custom join"""
        import mock

        return mock.MagicMock(name="{!r}.join".format(separator))
于 2014-05-22T11:42:02.237 回答