1

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

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

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

来自GitHub的代码:

缓存生成器/生成器.py:

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

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

缓存生成器/转换器.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 之间的一些变体看起来可以接受,但理想情况下我希望代码尽可能接近(与原始代码),所以我正在研究存根子对象的替代方法。

4

2 回答 2

1

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

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

然后你可以这样做:

修补调用_converter.convert

测试.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()

self._buildModifiers或者您可以修补对inside的调用Converter

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()
)
于 2013-01-27T20:49:04.560 回答
1

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

或者,查看依赖注入或为您构建s的工厂。ModifierBuilder

最后,您可以通过导入模块然后为符号分配新值来使用猴子补丁:

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

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

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

于 2013-01-23T11:11:40.713 回答