【问题标题】:Template method invocation order in pythonpython中的模板方法调用顺序
【发布时间】:2012-09-24 07:23:03
【问题描述】:

单元测试 run 函数的最简单方法是什么?这里我对 fun* 函数的作用不感兴趣,但前提是它们的调用顺序正确。

from mock import Mock

(many, arguments, four, fun) = ('many', 'arguments', 'four', 'fun') 

class TemplateMethod(object):

    def fun1(self, many, parameters, go, here):
        raise NotImplementedError()

    @classmethod
    def fun2(cls, many, parameters, go, here):
        pass

    @staticmethod
    def fun3(many, parameters, go, here):
        pass

    def run(self):
        result1 = self.fun1(many, arguments, four, fun)
        result1.do_something()

        self.fun2(many, arguments, four, fun)
        self.fun3(many, arguments, four, fun)

解决方案的唯一要求是对被测类的非侵入性。

解决方案:

实际上,这有点像草稿,这个简单的类可以通过多种方式进行修复和扩展,我什至都不想考虑它。关键是您现在可以简单地在模板方法中记录所有方法的所有调用。您还可以指定不应被该类模拟的对象列表(即您正在为其记录调用的函数)。

特别感谢@Damian Schenkelman,他给了我一些重要的建议。

class MockingInvocationRecorder(object):

    FUNCTION_TYPES = ('function', 'classmethod', 'staticmethod')

    def __init__(self, obj, dont_mock_list):
        self._invocation_list = []

        name_list = [exc.__name__ for exc in dont_mock_list]
        self._wrap_memfuns(obj, name_list)

    @property
    def invocations(self):
        return tuple(self._invocation_list)

    def _wrap_single(self, memfun, exclude_list):
        def wrapper(*args, **kwargs):
            self._invocation_list.append(memfun.__name__)

            if memfun.__name__ in exclude_list:
                return memfun(*args, **kwargs)
            else:
                return Mock()

        return wrapper

    def _wrap_memfuns(self, obj, exclude_list):
        for (mem_name, mem) in type(obj).__dict__.iteritems():
            if type(mem).__name__ in self.FUNCTION_TYPES:
                wrapper = self._wrap_single(getattr(obj, mem_name), exclude_list)
                setattr(obj, mem_name, wrapper)

您现在可以通过以下方式测试调用顺序:

tm = TemplateMethod()
ir = MockingInvocationRecorder(tm, [tm.run])

tm.run()

print ir.invocations => ('run', 'fun1', 'fun2', 'fun3')

您可以通过类名引用成员:

ir = MockingInvocationRecorder(tm, [TemplateMethod.run])

请注意您列出了所有不应被嘲笑的方法。

【问题讨论】:

  • 这让我觉得这是一个可疑的设计。为什么需要检查函数的顺序是否正确?如果以后对程序进行某些更改需要以不同的顺序执行函数怎么办?您必须基本上将更改复制到单元测试。作为测试的一部分,按照函数调用的顺序进行硬编码似乎违反了不要重复自己原则。必须有一种更好、更抽象的方法来测试run 的输出,而不用关心它是如何获得输出的,不是吗?
  • 关键是要确保模板方法中的算法步骤实际上是按特定顺序执行的。在我的实际代码中,我首先执行身份验证和授权。至于设计,这只是一个 POC。我不喜欢拥有 call_appender 类的想法,但由于我是 Python 的新手,我看不到任何更简单的解决方案。我可以通过反射扩展当前的解决方案,但我有点希望有人会提出更好的设计。 (我不是要求完整的解决方案,只是想法。)

标签: python unit-testing template-method-pattern


【解决方案1】:

Monkeypatch fun1、fun2、fun3 函数并使用列表来跟踪调用顺序。之后,断言就行了。

【讨论】:

  • 如何使用列表来记录通话?
  • 在测试方法中声明一个列表,在每个monkeypatching lambda中你可以简单地将1添加到列表中以获得fun1,2添加到fun2等......
  • 我已经尝试过 lambdas,但问题是我不知道如何将列表传递给它们,除非作为参数。除了。只有在run 方法中未使用模拟函数的返回值时,lambdas 才能用于模拟(如在我的示例中,但我在这里的目标是一些一般性),因为append 的返回值是 None,IIRC。
  • 一个 lambda (或闭包)可以访问在它被定义的范围内定义的变量,因此您不需要将列表作为参数传递。例如:fun1 = lambda: invocations.append("fun1") 可以使用,在单元测试中将调用声明为:invocations = []。
  • 好的,lambda 是 python 中的闭包......不知道。谢谢。现在我只需要找到一个允许我在 lambda 中执行两个语句的 hack。在更新版本的代码中,您可以看到 do_something 在其中一个函数的返回值上被调用。 lambda 中的条件表达式是否可以实现这一点?像这样的东西:lambda *argv, **kwargv: SomeClass() if invocations.append('fun1') is None else SomeClass()?是否有其他方法可以达到相同的效果,即返回一些特定的值并附加到调用?
【解决方案2】:

在这里使用__call__ 可能会有所帮助。

class Function1(object):
    def __call__(self, params, logger=None):
        if logger is not None:
            logger.append(self.__repr__())
        return self.function1(params) 
        # Or no return if only side effects are needed

    def __init__(self, ...):
        #...

    def __repr__(self):
        # Some string representation of your function.

    def function1(params):
        print "Function 1 stuff here..."

然后做类似的事情

class TemplateMethod(object):
    def __init__(self, params):
        self.logger = []
        self.fun1 = Function1(...)
        self.fun1(params, self.logger)

这很老套;可能有一些方法可以清理我正在处理的内容,但是将函数封装在类中然后使用 __call__ 是一个不错的方法。

【讨论】:

  • 我喜欢这个想法,但这种方法的问题是您正在修改被测类以适应测试,而不是相反。我一直在寻找一种非侵入式的解决方案。
  • 是的,但是如果您给TemplateMethod 一个记录器,那么在您的单元测试代码中,您可以提取该记录器在测试时应该是什么样子的规范,而无需让测试代码知道函数的名称或调用它们的顺序。这都可以是元数据,它本身可以从TemplateMethod 的源代码、规范或文档中提取。不过,我会对直接提取 TemplateMethod 的函数调用的其他解决方案感兴趣。
  • 是的,我同意。顺便说一句,您的方法可以更顺畅地与装饰器融合。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-21
  • 2014-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-21
  • 1970-01-01
相关资源
最近更新 更多