[E]ven internal APIs still need to be read / used by developers and I
don't like screwing with them either.
...
My contention is that dependency injection just for testability
is never required for Python and rarely preferable to other
techniques. There are plenty of times when dependency injection is
useful as a structure / architecture in its own right though.
When I sit down and look at my code and think, 'How could I test this better?' and I change the product code to make it more testable, it's actually better product code. And I think that's because, if you have to write something that does one thing, it can do that one thing well, but if you write something that can do two things well, that's a better thing. And doing testing well, in addition to being a good product, makes the code better. By having two uses, you have to really think about that API, and you have to really think about what that one piece of code is doing. And by having it do both of those things well, you've got a better, modular, more abstraction design, that's going to be better for your product in the long run. So most of what you do for testability is going to be good for the product in addition to all the other good things about testability finding more bugs and having better quality and all that stuff.
In real code I tend to mark "test hook only" parameters with an underscore
prefix - so the signature would be:
__init__(self, rel_url, _urlopen=urlopen)
And then in the docstring for the method, I make clear it's a testing hook
that is liable to go away without warning. (Yes, I'll be sure to write a
docstring in this case :) The underscore is just my way of highlighting the
parameter as special in some way.
Of course, that's if I want it to only be used for testing. If it is
something we decide we want to make available outside of that context, and
commit to keeping around, I won't put up "keep off" signs like that :)
If it's controlling a critical piece of behavior, like where it's going to write its persistent data, we've found that it's much safer to make it a required argument and just always specify it. We've found that in cases of object relationship where the first object does not make sense unless it also has a second so, a user profile doesn't make sense unless it has a user credential we've found that explicit construction parameters is the most robust solution for us...[Optional parameters] are good for things that change the behavior of your object in a subtle way.
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
The principle inverts the way some people may think about object-oriented design, dictating that both high- and low-level objects must depend on the same abstraction.
advantages of dependency injection,可以被提炼为可重用性。无论是通过全局配置、动态算法还是不断发展的应用程序开发,解耦从函数/方法/类的依赖实现细节中抽象出一个函数/方法/类,允许这些组件中的每一个(尽管在这种情况下,特别是抽象)在编写时没有计划的组合,没有修改。测试就是一个很好的例子,因为可测试性就是可重用性。在
所有这些理论的实际意义在于,将无法进行单元测试的“粘合代码”与逻辑分离开来,而逻辑正是您想要进行单元测试的。虽然他们对一个规范特别感兴趣为了实现这一原则,我认为法克勒和玛尼斯塔给出了a good example of this。如果有人可能会:
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
他们会建议:
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)
I believe in strong testing of behavioral parts of software, so things like functions, things like computations, things like state changes, and I don't believe in unit tests for assemblies: when you're starting up your application, when you're connecting one object to another, or when you're constructing something what you might think of as glue code. It's simple enough, and it's going to be covered by your integration tests. This contemporary class example (NewClass): we might not write a test for that because there's really no interesting logic in there other than the side effect of setting an attribute...We've already written a test for the pure function that we factored out of that method, so there's not a whole lot of incremental benefit to testing this instance method as well.
尽可能频繁地使用DI是很有用的,但有时它并不可行, 因为你:
那时候你就得靠猴子打补丁了。在
你应该能够避免它在几乎所有的情况下,理论上你可以避免它100%,但有时它只是更合理的猴子补丁例外。在
注意:这个问题的任何答案都同样适用于传递其他类型的双打(mock、fake、stub等)。
关于这个话题有很多宗教信仰,所以这个问题的标准答案是:“做对你的应用程序实用的事情,利用你的代码嗅觉。”虽然我也倾向于拒绝非此即彼的方法,但这个答案对任何真正提出问题的人(即我自己)来说,本质上是毫无用处的。以下是我使用的决策过程,以及我在开发过程中所做的一些考虑:
事先评估
嘲笑策略
术语
依赖注入:在Python上下文中,这个术语通常专门指constructor injection。在
Monkey在运行时将:Binding a name(在测试代码中)修补到与模块中绑定到的对象不同的对象。实际上,这通常意味着使用^{} 。在
示例
假设我们有一个在测试过程中有一个不受欢迎的副作用的函数,不管它是破坏性的(向我们的生产数据库中写无意义的东西)还是烦人的(例如,慢)。下面是后一种情况的一个例子及其测试:
我们的测试有效,但至少需要5秒钟。我们可以通过将
time.sleep
替换为不起任何作用的a mock object来避免等待。这两种策略是本问题的主题:依赖注入
^{pr2}$猴子修补
^{3}$为什么是依赖注入?在
我发现monkey补丁的利弊更为直接,所以我将分析重点放在依赖注入上。在
杂乱的接口只是以提高可测试性?在
依赖注入非常明确,但需要修改产品代码。但是猴子不需要修改产品的补丁代码。在
程序员的本能反应是在为测试修改产品代码之前做出许多牺牲。在考虑了注入所有依赖项后,您的应用程序将是什么样子之后,对monkey补丁的偏好似乎是一个无需考虑的问题。作为Michael Foord expresses:
当编写单元测试时,这个话题自然会出现,但对那些提倡依赖注入的人的善意解释得出结论,可测试性并不是他们的主要动机。Ned Batchelder finds (25:30)这种猴子式的修补“让代码有点难以理解,我宁愿在某个地方看看:我们现在正在测试,所以这就是你获得时间的方式。”他阐述(3:14):
界面污染问题
不,不仅仅是视觉污染。假设我们认识了蒂姆e在角落情况下,我们需要一个更复杂的算法来确定上述
foo
函数中的等待时间:但是过了一段时间,对于我们主要使用的
foo
,等待就变得不必要了。我们一直使用依赖注入模式,所以我们假设可以删除命名参数而不产生后果。在我们现在需要追踪我们的角落案例,以避免打字错误。似乎即使这些参数只是为了测试目的而添加的,它们仍将在生产中使用,而且这种方法通过在接口中放置实现细节来限制重构的能力。在
但是Aaron Maxwell has a solution:
虽然这种方法确实解决了污染问题,但对我来说,所有这些杂乱无章的事情首先是增加了接口,其次是确保您不实际使用接口,这都是一种气味。在
但是Augie Fackler and Nathaniel Manista's position需要的位置参数比可选的关键字参数更安全,这将使污染问题变得没有意义。They elaborate:
在不考虑评估他们更广泛的测试策略的情况下,我们应该很容易同意的一点是,关键组件不应作为可选参数传递给。在
然而,我不明白为什么“关键”依赖不应该硬编码,当关系是一个特定的依赖时。抽象的实质是它与其他抽象的关系。因此,如果抽象的一个基本属性是与应用程序中的另一个抽象的关系,那么它是硬编码的首选,不管两个抽象中的实现细节有多大的变化,它们都是永久耦合的。在
区别的一部分是对系统造成风险的依赖项和没有风险的依赖项。如果依赖关系负责写入数据库、推送客户机或投放炸弹,那么除非我们编写无人机软件,否则我们无法负担mistakes。在
值得注意的是,注入位置论据会使“观望”策略代价高昂。如果我们决定某一天需要在构造函数中选择一个硬编码的依赖项,那么将其作为位置参数添加会破坏向后兼容性。如果我们后来决定删除一个必需的参数,也会遇到类似的问题,因此非必要的依赖项必须是可选参数,这样我们就可以自由地更改接口。在
依赖关系不好,mkay?在
Constructor injection是依赖注入的几种方法之一。根据Wikipedia的说法:“依赖注入是一种实现控制反转的软件设计模式,允许程序设计遵循依赖反转原则。”
一。在
这和我想用这个术语得到的一样具体,给定the disputed status of it's value。但正是这些类型的担忧激发了Martelli(evidenced by his 2007 talk especially)。在
advantages of dependency injection,可以被提炼为可重用性。无论是通过全局配置、动态算法还是不断发展的应用程序开发,解耦从函数/方法/类的依赖实现细节中抽象出一个函数/方法/类,允许这些组件中的每一个(尽管在这种情况下,特别是抽象)在编写时没有计划的组合,没有修改。测试就是一个很好的例子,因为可测试性就是可重用性。在
还记得为了满足测试代码的需要而改变产品代码的本能反应吗?好吧,为了满足生产实现的特殊需求,您也应该对更改抽象做出相同的反应!在
所有这些理论的实际意义在于,将无法进行单元测试的“粘合代码”与逻辑分离开来,而逻辑正是您想要进行单元测试的。虽然他们对一个规范特别感兴趣为了实现这一原则,我认为法克勒和玛尼斯塔给出了a good example of this。如果有人可能会:
他们会建议:
为了测试
EnLolCat
,我们可能发现自己在类中使用了粘合代码,并且可以很容易地测试一个自由函数,因为它没有任何副作用,而且是完全确定的。换句话说,我们正在做更多的事情。在但是,当我们测试
NewClass
方法时,我们的情况不是一样的吗?Not necessarily。在依赖关系对测试中的代码是不好的,所以如果每个依赖项都使代码变得更难看一点也许是件好事。丑陋的成本比紧耦合的成本低,但开发者更可能认真对待这一点。在
最后说明
相关问题 更多 >
编程相关推荐