lex*_*ope 9 python tdd unit-testing mocking python-mock
我可以看到两种不同的方法将mocks注入到我想要测试的python代码中:
依赖注入:
允许协作类传递到被测对象的构造函数中,并传入模拟对象(以及必要时的工厂,如Java)
猴子补丁:
使用模拟对象工厂来锁定被测模块中的协作类(以便构建协作类实际上创建了一个模拟对象).我不需要允许它们通过构造函数注入或创建任何工厂.
python 模拟库(例如,mox或mock)似乎支持这两种方法.我应该在Python中使用哪种方法,这些是合理的还是有更好的方法?
Ryn*_*ett 11
注意:对这个问题的任何答案都同样适用于传递其他类型的双打(模拟,假货,存根等).
关于这个话题有很多宗教信仰,所以这个问题的标准答案是:"为你的应用做些什么是实用的,并使用你的代码味道." 虽然我也倾向于拒绝这两种方法,但这个答案让我觉得对任何实际问这个问题的人来说都是无用的(即我自己).这是我正在使用的决策过程,以及我在其开发过程中所做的一些考虑:
依赖注入:在Python的上下文中,这个术语通常特指构造函数注入.
Monkey Patching:将名称(在测试代码中)绑定到运行时中的不同对象,而不是绑定到模块中.在实践中,这通常意味着使用mock.patch.
假设我们有一个副作用的功能在测试期间是不受欢迎的,无论是破坏性的(对我们的生产数据库写废话)还是讨厌(即慢).以下是后一种情况及其测试的示例:
def foo():
...
time.sleep(5)
...
return "bar"
...
def foo_test():
assertEqual(foo(), "bar")
Run Code Online (Sandbox Code Playgroud)
我们的测试工作,但至少需要五秒钟.我们可以通过更换避免等待time.sleep与模仿对象,什么也不做.这样做的两个策略是这个问题的主题:
def foo(wait=time.sleep):
...
wait(5)
...
return "bar"
...
def foo_test():
assertEqual(foo(wait=mock.Mock()), "bar")
Run Code Online (Sandbox Code Playgroud)
def foo():
...
time.sleep(5)
...
return "bar"
...
@mock.patch('time.sleep')
def foo_test():
assertEqual(foo(), "bar")
Run Code Online (Sandbox Code Playgroud)
我发现猴子修补的优点和缺点更直接,所以我将注意力集中在依赖注入上.
依赖注入非常明确,但需要修改产品代码.Monkey修补不明确,但不需要修改产品代码.
程序员的直觉反应是在修改测试的产品代码之前做出许多牺牲.在考虑了所有依赖项注入后应用程序的外观之后,对猴子修补的偏好似乎是不费脑子的.正如Michael Foord所表达的那样:
[E]内部API仍然需要开发人员阅读/使用,我也不喜欢与他们搞砸.
...
我的论点是Python 不需要依赖注入只是为了可测试性而且很少优于其他技术.虽然依赖注入有很多时候作为结构/体系结构很有用.
虽然在编写单元测试时自然会出现这个主题,但对那些主张依赖注入的人的慈善解释得出的结论是,可测试性不是他们的主要动机.Ned Batchelder发现(25:30)猴子修补"使代码难以理解,我宁愿在某处看到:我们现在正在测试,所以这就是你有时间的." 他阐述了(3:14):
当我坐下来看看我的代码并思考,"我怎么能更好地测试它?" 并且我更改了产品代码以使其更易于测试,它实际上是更好的产品代码.而且我认为那是因为,如果你必须写一些做一件事的东西,它可以很好地做到这一点,但是如果你写的东西可以做好两件事,那就更好了.并且做好测试,除了是一个好产品,使代码更好.通过两次使用,您必须真正考虑该API,并且您必须真正考虑一段代码正在做什么.通过让它完成这两件事,你就拥有了更好的,模块化的,更抽象的设计,从长远来看,这对你的产品来说会更好.因此,除了可测试性的所有其他好处之外,您为可测试性所做的大部分工作都将对产品有益 - 发现更多错误并提供更好的质量和所有这些东西.
不,不只是视觉污染.假设我们随着时间的推移意识到,在一个极端情况下,我们需要一个更复杂的算法来确定foo上面函数中的等待时间:
-- bar = foo()
++ bar = foo(wait=mywait)
Run Code Online (Sandbox Code Playgroud)
但是一段时间之后,等待我们的主要用途就变得不必要了foo.我们一直使用依赖注入模式,因此我们假设我们可以删除没有结果的命名参数.
-- def foo(wait=time.sleep):
++ def foo():
Run Code Online (Sandbox Code Playgroud)
我们现在需要跟踪我们的角点情况以避免TypeError.看起来即使这些参数仅用于测试目的,它们也将用于生产,并且这种方法通过在接口中放置实现细节来限制您重构的能力.
在实际代码中,我倾向于使用下划线前缀标记"仅测试挂钩"参数 - 因此签名将是:
__init__(self, rel_url, _urlopen=urlopen)然后在该方法的文档字符串中,我明确表示它是一个可以在没有警告的情况下消失的测试挂钩.(是的,我肯定会在这种情况下写一个文档字符串:)下划线只是我以某种方式突出显示参数的方式.当然,如果我希望它只用于测试.如果这是我们决定在这种背景之外提供的东西,并承诺保持不变,我不会提出像这样的"保持"标志:)
虽然这种方法确实解决了污染问题,但对我来说,所有这些混乱 - 首先添加到界面,第二个确保您实际上不使用界面 - 有一种气味.
但是,Augie Fackler和纳撒尼尔·马尼斯塔的立场要求,位置论证比可选的关键词论证更安全,这会使污染问题没有实际意义.他们详细说明:
如果它正在控制一个关键的行为,比如它将编写持久数据的位置,我们发现将它作为必需参数并且始终指定它更安全.我们发现,在对象关系的情况下,第一个对象没有意义,除非它还有第二个 - 因此,除非用户凭证具有用户凭证,否则用户配置文件没有意义 - 我们发现它是显式的构造参数是我们最强大的解决方案...... [可选参数]适用于以微妙方式改变对象行为的事物.
在不评估其更广泛的测试策略的情况下,我们应该容易同意的一点是关键组件不应该作为可选参数传递.
但是,当关系具有特定的依赖关系时,我不明白为什么"关键"依赖关系不应该被硬编码 .抽象的实质是它与其他抽象的关系.因此,如果抽象的基本属性是与应用程序中的另一个抽象的关系,那么它就是硬编码的主要候选者 - 无论实现细节在抽象中有多少变化,它们都是永久耦合的.
所做出的部分区别在于对系统构成风险的依赖关系和不对系统构成风险的依赖关系.如果依赖性负责写入数据库,推送到客户端或丢弃炸弹,那么除非我们编写无人机软件,否则我们不能犯错误.
值得注意的是,注入位置参数会使"等待观察"策略成本高昂.如果我们决定某天需要在构造函数中选择我们的一个硬编码依赖项,那么将它作为位置参数添加会破坏向后兼容性.如果我们后来决定删除必需的参数,则会遇到类似的问题,因此非必要的依赖项必须是可选参数,因此我们可以自由地更改接口.
构造函数注入是依赖注入的几种方法之一.根据维基百科:"依赖注入是一种软件设计模式,它实现了控制的反转,并允许程序设计遵循依赖性反转原则."
控制反转服务于以下设计目的:
- 将任务的执行与实现分离.
- 将模块专注于它的设计任务.
- 使模块免于假设其他系统如何做他们所做的事情,而是依赖合同.
- 更换模块时防止副作用.
.
依赖倒置原则的目标是将应用程序粘合代码与应用程序逻辑分离...
该原则规定:
A.高级模块不应该依赖于低级模块.两者都应该取决于抽象.B.抽象不应该依赖于细节.细节应取决于抽象.
原则反转有些人可能会认为面向对象的设计方法,即口述既高,低级别的对象必须依赖于相同的抽象.
鉴于其价值的争议状态,这就像我想要使用这个术语一样具体.但正是这些类型的关注激发了马尔泰利(特别是2007年的谈话证明).
依赖注入的优点,可以提炼为可重用性.无论是通过全局配置,动态算法,或发展的应用开发,解耦从实现细节的功能/方法/类的抽象的它的依赖使得这些部件的每(虽然在此情况下,特别是抽象)的电势,以在没有修改的情况下,在编写时无需计划的组合.测试就是一个例子,因为可测试性是可重用性.
还记得对改变产品代码以满足测试代码需求的直觉反应吗?那么你应该对改变你的抽象做出同样的反应,以满足生产实施的特殊需求!
所有这些理论的实际意义在于将"胶合代码"(不能进行单元测试)与逻辑分开,这就是您想要进行单元测试的.虽然他们对这一原则的具体实施特别感兴趣,但我认为Fackler和Manista就是一个很好的例子.人们可能拥有的地方:
class OldClass(object):
...
def EnLolCat(self, misspelt_phrases):
lolcats = []
for album in self.albums:
for photo in album.Photos():
exif = photo.EXIF()
for text in misspelt_phrases:
geoText = text.Geo(exif)
if photo.canCat(geoText)
lolcat = photo.Cat(geoText)
self.lolcats = lolcats
Run Code Online (Sandbox Code Playgroud)
他们建议:
def Lol(albums, misspelt_phrases):
lolcats = []
for album in albums:
for photo in album.Photos():
exif = photo.EXIF()
for text in misspelt_phrases:
geoText = text.Geo(exif)
if photo.canCat(geoText)
lolcat = photo.Cat(geoText)
return lolcats
class NewClass(object):
...
def EnLolCat(self, misspelt_phrases):
self.lolcats = Lol(
self.albums, misspelt_phrases)
Run Code Online (Sandbox Code Playgroud)
在我们可能发现自己嘲笑对象实例以进行测试的地方EnLolCat,我们现在发现自己在我们的类中使用胶水代码,并且我们可以轻松测试自由函数,因为它没有副作用并且是完全确定的.换句话说,我们正在做更多的函数式编程.
但是,在测试NewClass方法时,我们的情况不一样吗?不一定.
我相信对软件行为部分的强大测试,所以像函数,计算之类的东西,状态变化之类的东西,我不相信程序集的单元测试:当你启动你的应用程序时,当你是将一个对象连接到另一个对象,或者当您构建某些东西时 - 您可能会将其视为胶水代码.这很简单,它将由集成测试覆盖.这个当代类的例子(
NewClass):我们可能不会为此编写测试,因为除了设置属性的副作用之外,其中没有任何有趣的逻辑...我们已经编写了一个测试我们考虑的纯函数那个方法,所以测试这个实例方法也没有很多增量上的好处.
依赖关系对于被测代码是不好的,所以如果每个代码都让你的代码变得更加丑陋,那么这可能是件好事.丑陋的成本低于紧耦合的成本,但开发人员更有可能认真对待.
尽可能经常使用 DI 通常很有用,但有时这是不可行的,因为您:
这就是你必须求助于猴子补丁的时候。
您应该能够在几乎所有情况下避免它,理论上您可以 100% 避免它,但有时制作猴子补丁例外更为合理。