一种用桩替换实际组件的方法
我在整理代码时遇到了一些问题,想让它更容易测试。我有两个主要模块:缓存生成器和修改器构建器,它们的复杂程度差不多。修改器构建器是在缓存生成器的子对象中的一个方法里使用的。
我已经有了一个完整的测试套件,覆盖了修改器构建器的功能。我想添加一些测试,覆盖缓存生成器的所有功能,但为了大大降低这些测试的复杂性,我需要用一个“替身”来替换修改器构建器,这个替身会根据我传给它的参数返回预设的数据。
我真正的问题在于如何用替身替换掉真实的修改器构建器,既要代码看起来不错,又要方便测试。看看下面的代码:
来自 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(...)
替换修改器构建器为替身的方法有哪些呢?我想至少有以下几种选择:
- 修改代码:在转换器的 init() 方法中实例化修改器构建器,并将其实例作为对象属性。为了测试,创建一个真实转换器的子类,重写 init(),在这里用替身替换真实的修改器构建器,然后再对子类化缓存生成器,以类似的方式替换真实的转换器。不过,这种方法需要修改修改器构建器:我需要将数据加载从 init() 方法中分离出来,这样做不太理想。
- 和第1种方法类似,但将处理修改器构建器的 Converter()._buildModifiers() 方法中的部分代码移动到单独的方法中,以便更容易重写。
- 和第1种方法类似,但在清理器的 init() 中只指定修改器构建器的类(而不是实例)。这样可以保持修改器构建器不变。
- 从缓存生成器的最上层传递修改器构建器类(这样我们需要替换的类可以通过缓存生成器的实例化来控制)。
- 还有其他选择吗?比如一些导入的魔法?
在1到4种选择中,有些看起来可以接受,但理想情况下我希望代码尽量保持接近原样,所以我在寻找替身化子对象的其他方法。
2 个回答
当我在测试中需要模拟或伪造对象时,我会使用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伪造对象。
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()
)
我通常更喜欢第二种方法,因为这样可以让意图更清晰,这只是一个小改动,而且可能对其他使用我代码的地方有帮助。
另外,你可以看看依赖注入或者一个工厂模式,它可以为你构建ModifierBuilder
。
最后,你可以通过导入模块来使用猴子补丁,然后给符号赋一个新值:
import cacheGenerator.converter
cacheGenerator.converter.ModifierBuilder = ...
当然,这样会改变所有人的符号(也就是说,其他所有测试也会受到影响),所以你需要保存旧值,并在测试后恢复它。
如果你对这个解决方案感到不安,那你是对的:这是一种绝望的措施。如果你真的无法更改原始代码,可以把它当作最后的手段来使用。