【问题标题】:A way to replace actual component with a stub一种用存根替换实际组件的方法
【发布时间】:2013-01-06 19:59:41
【问题描述】:

我在安排代码以使其易于测试时遇到问题。我的代码中有 2 个主要模块:缓存生成器和修改器生成器,两者的复杂程度大致相同。修饰符builder用于缓存生成器的子对象的方法之一。

我已经拥有完整的测试套件,其中涵盖了修饰符生成器的功能。我想添加涵盖缓存生成器所有功能的测试,但为了显着降低这些测试的复杂性,我需要用一些存根替换修饰符生成器,​​它根据我传递给它的参数返回预定义的“罐头数据”。

我的实际问题在于选择用存根替换真实修饰符构建器的方式,这在代码方面看起来不错,并且仍然便于测试。看看下面的代码:

来自GitHub的代码:

cacheGenerator / generator.py:

class CacheGenerator:
    def __init__(self, logger):
        ...
        self._converter = Converter(logger)

    def run(self, dataHandler):
        ...
        data = self._converter.convert(data)

cacheGenerator/converter.py:

class Converter:
    ...

    def convert(self, data):
        ...
        self._buildModifiers(data)

    def _buildModifiers(self, data):
        ...
        builder = ModifierBuilder(data['expressions'], self._logger)
        ...
           modifiers, buildStatus = builder.buildEffect(...)

用存根替换修饰符生成器的方法是什么?我想至少存在以下几个变体:

  1. 对代码的更改:在转换器的 init() 中实例化修饰符生成器并将其实例分配为对象属性。对于测试 - 创建真正转换器的子类,覆盖 init(),我们用存根替换真正的修饰符生成器,​​然后子类化缓存生成器,以类似的方式用子类替换真正的转换器。但是,这种方法需要修改修饰符生成器:我需要从 init() 方法中拆分数据加载,这是不可取的
  2. 与 1) 类似,但将与修改器生成器一起使用的 Converter()._buildModifiers() 方法的部分内容移动到单独的方法中,以使它们易于被覆盖
  3. 类似于 1),但在清洁器的 init() 中仅指定修饰符生成器类(而非实例)。这使我们可以保持修饰符生成器不变。
  4. 从缓存生成器的最顶部传递修饰符构建器类(以便我们需要替换以进行测试的类可以通过缓存生成器实例化来控制)
  5. 还有其他变种吗?喜欢一些进口魔法?

1-4 中的一些变体看起来可以接受,但理想情况下,我希望代码尽可能接近(与原始代码),因此我正在研究存根子对象的替代方法。

【问题讨论】:

    标签: python unit-testing python-3.x


    【解决方案1】:

    当我需要在测试中模拟/伪造对象时,我使用Fudge

    在您的情况下,我建议使用patched_context。有了它,您可以修补对Converter 方法的调用。

    然后你可以这样做:

    修补对_converter.convert的调用

    test.py:

    from cacheGenerator.generator import CacheGenerator
    from cacheGenerator.converter import Converter
    from fudge import patched_context
    
    import unittest
    
    class Test_cacheGenerator(unittest.testCase):
    
        def test_run(self):
    
            def fakeData(convertself, data):
                # Create data to be returned to 
                # data = self._converter.convert(data)
                fakedata = ...
                return fakedata
    
    
            # We tell Fudge to patch the call to `Converter.convert`
            # and instead call our defined function 
            cache = cacheGenerator(...)
            with patched_context(Converter, 'convert', fakeData)
                cache.run()
    

    或者您可以在Converter 中修补对self._buildModifiers 的调用:

    def test_run(self):
            cache = cacheGenerator(...)
    
            def fakeBuildModifiers(convertself, data):
                # set up variables that convert._buildModifiers usually sets up
                convertself.modifiers = ...
                convertself.buildStatus = ...
    
            # We tell Fudge to patch the call to `Coverter._buildModifiers`
            # and instead call our defined function 
            cache = cacheGenerator(...)
            with patched_context(Converter, '_buildModifiers', fakeBuildModifiers):
                cache.run()
    

    或者,您也可以使用Fudge fake object

    from fuge import Fake
    
    ...
        def test_run(self):
            cache = cacheGenerator(...)
    
            fakeData = ...
            fakeConverter = Fake('Converter').provides('convert').returns(fakeData)
    
            # Fake our `Converter` so that our any calls to `_converter.convert` are
            # made to `fakeConverter.convert` instead.
            cache._converter = fakeConverter
    
            cache.run()
    

    在最后一种情况下,由于您要修补整个 _converter 对象,如果您要调用任何其他方法,则还需要修补它们。

    (Fake('Converter'.provides('convert').returns(fakeData)
                     .provides(....).returns()
                     .provides(....).returns()
    )
    

    【讨论】:

      【解决方案2】:

      我通常更喜欢 2,因为它使意图清晰,是一个小改动,它可能对重用我的工作的其他代码有用。

      或者,查看dependency injection 或为您构建ModifierBuilders 的factory

      最后,您可以通过导入模块然后为符号分配新值来使用monkey patching

      import cacheGenerator.converter
      cacheGenerator.converter.ModifierBuilder = ...
      

      当然,这会更改每个人的符号(即也适用于所有其他测试),因此您需要保存旧值并在测试后恢复它。

      如果您对这个解决方案感到不好/不安,那么您是对的:这是一个绝望的措施。如果您确实无法更改原始代码,请使用它作为最后的手段。

      【讨论】:

      • 注意:我不是 100% 确定嵌套的导入语法 :-)
      • 好吧,我也想不惜一切代价避免猴子补丁。通过说“导入魔术”,我的意思是更面向测试的东西,比如整个虚拟命名空间,我可以在其中有选择地从文件系统导入真实模块,并手动定义我想用作存根的模块。万一,你知道这种可能性是否存在吗?
      • 为“依赖注入”提供的链接指向特定的 DI 框架。我更喜欢手动进行 DI。更喜欢构造函数注入,但你也可以使用 setter 注入。
      • @DarkPhoenix:您可以将这些模拟模块放入项目默认源文件夹之外的某些文件夹中(即 Python 可以查看的任何地方)。这将允许您修改 sys.path 以使 Python 加载您的模拟。不幸的是,这只适用于一次;之后的任何import 都将获得相同的模拟,您不能从import 缓存中“刷新”模块。您可以使用像 pyplugin 这样的插件框架,但这也意味着要更改代码。
      猜你喜欢
      • 2014-12-25
      • 2015-04-09
      • 2020-09-07
      • 2010-12-20
      • 2023-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-28
      相关资源
      最近更新 更多