我应该如何使用Python中的mock进行测试?

2024-06-08 07:45:44 发布

您现在位置:Python中文网/ 问答频道 /正文

我可以看到两种不同的方法将mock注入到我想要测试的python代码中:

  1. 依赖注入:

    允许协作类传递到被测对象的构造函数中,并传入模拟对象(以及必要时的工厂,如Java)

  2. 猴子修补:

    用一个模拟对象工厂在被测模块中断开协作类(这样构建协作类实际上会创建一个模拟对象)。我不需要允许它们通过构造函数注入或者创建任何工厂。

这两种方法似乎都受到python模拟库(例如,moxmock)的支持。我应该在Python中使用哪种方法?这些方法中的一种是合理的还是有更好的方法?在


Tags: 模块对象方法代码工厂javamock猴子
2条回答

尽可能频繁地使用DI是很有用的,但有时它并不可行, 因为你:

  • 使用内置函数或对象(如文件)
  • 第三方功能
  • 使用不确定的对象/调用等

那时候你就得靠猴子打补丁了。在

你应该能够避免它在几乎所有的情况下,理论上你可以避免它100%,但有时它只是更合理的猴子补丁例外。在

注意:这个问题的任何答案都同样适用于传递其他类型的双打(mock、fake、stub等)。

关于这个话题有很多宗教信仰,所以这个问题的标准答案是:“做对你的应用程序实用的事情,利用你的代码嗅觉。”虽然我也倾向于拒绝非此即彼的方法,但这个答案对任何真正提出问题的人(即我自己)来说,本质上是毫无用处的。以下是我使用的决策过程,以及我在开发过程中所做的一些考虑:

事先评估

  1. 您的代码能从更多的函数式编程中获益吗?如果你能排除依赖性和副作用,你就不需要嘲笑任何东西。在
  2. 单元是否可以进行有意义的单元测试?也许它需要重构,或者它只是“粘合代码”,无法进行有效的单元测试(但是必须被系统测试覆盖)。在
  3. 你真的需要嘲笑这种依赖吗?如果运行真正的代码仍然可以让您有意义地测试行为,并且既不缓慢也不具有破坏性,那么就让它运行吧。在

嘲笑策略

  • 如果特定的依赖关系是essential到单元,在产品中硬编码,在测试中使用monkey-patch。
    • 异常:如果在测试中使用生产依赖项可能对系统有害,请将其作为位置参数注入。在
    • Exception:如果monkey-patch有应用于另一个单元的依赖项的风险,并且这是不需要的,那么将其作为位置参数注入。在
  • 如果相同类型的依赖关系对单元是必需的,将其作为必需的位置参数注入。在
  • 如果依赖关系对单元不重要,将其作为可选关键字参数注入。在

术语

依赖注入:在Python上下文中,这个术语通常专门指constructor injection。在

Monkey在运行时将Binding a name(在测试代码中)修补到与模块中绑定到的对象不同的对象。实际上,这通常意味着使用^{}。在

示例

假设我们有一个在测试过程中有一个不受欢迎的副作用的函数,不管它是破坏性的(向我们的生产数据库中写无意义的东西)还是烦人的(例如,慢)。下面是后一种情况的一个例子及其测试:

def foo():
    ...
    time.sleep(5)
    ...
    return "bar"

...

def foo_test():
    assertEqual(foo(), "bar")

我们的测试有效,但至少需要5秒钟。我们可以通过将time.sleep替换为不起任何作用的a mock object来避免等待。这两种策略是本问题的主题:

依赖注入

^{pr2}$

猴子修补

^{3}$

为什么是依赖注入?在

我发现monkey补丁的利弊更为直接,所以我将分析重点放在依赖注入上。在

杂乱的接口只是以提高可测试性?在

依赖注入非常明确,但需要修改产品代码。但是猴子不需要修改产品的补丁代码。在

程序员的本能反应是在为测试修改产品代码之前做出许多牺牲。在考虑了注入所有依赖项后,您的应用程序将是什么样子之后,对monkey补丁的偏好似乎是一个无需考虑的问题。作为Michael Foord expresses

[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.

当编写单元测试时,这个话题自然会出现,但对那些提倡依赖注入的人的善意解释得出结论,可测试性并不是他们的主要动机。Ned Batchelder finds (25:30)这种猴子式的修补“让代码有点难以理解,我宁愿在某个地方看看:我们现在正在测试,所以这就是你获得时间的方式。”他阐述(3:14):

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.

界面污染问题

不,不仅仅是视觉污染。假设我们认识了蒂姆e在角落情况下,我们需要一个更复杂的算法来确定上述foo函数中的等待时间:

  bar = foo()
++ bar = foo(wait=mywait)

但是过了一段时间,对于我们主要使用的foo,等待就变得不必要了。我们一直使用依赖注入模式,所以我们假设可以删除命名参数而不产生后果。在

  def foo(wait=time.sleep):
++ def foo():

我们现在需要追踪我们的角落案例,以避免打字错误。似乎即使这些参数只是为了测试目的而添加的,它们仍将在生产中使用,而且这种方法通过在接口中放置实现细节来限制重构的能力。在

但是Aaron Maxwell has a solution

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 :)

虽然这种方法确实解决了污染问题,但对我来说,所有这些杂乱无章的事情首先是增加了接口,其次是确保您不实际使用接口,这都是一种气味。在

但是Augie Fackler and Nathaniel Manista's position需要的位置参数比可选的关键字参数更安全,这将使污染问题变得没有意义。They elaborate

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.

在不考虑评估他们更广泛的测试策略的情况下,我们应该很容易同意的一点是,关键组件不应作为可选参数传递给。在

然而,我不明白为什么“关键”依赖不应该硬编码,当关系是一个特定的依赖时。抽象的实质是它与其他抽象的关系。因此,如果抽象的一个基本属性是与应用程序中的另一个抽象的关系,那么它是硬编码的首选,不管两个抽象中的实现细节有多大的变化,它们都是永久耦合的。在

区别的一部分是对系统造成风险的依赖项和没有风险的依赖项。如果依赖关系负责写入数据库、推送客户机或投放炸弹,那么除非我们编写无人机软件,否则我们无法负担mistakes。在

值得注意的是,注入位置论据会使“观望”策略代价高昂。如果我们决定某一天需要在构造函数中选择一个硬编码的依赖项,那么将其作为位置参数添加会破坏向后兼容性。如果我们后来决定删除一个必需的参数,也会遇到类似的问题,因此非必要的依赖项必须是可选参数,这样我们就可以自由地更改接口。在

依赖关系不好,mkay?在

Constructor injection是依赖注入的几种方法之一。根据Wikipedia的说法:“依赖注入是一种实现控制反转的软件设计模式,允许程序设计遵循依赖反转原则。”

Inversion of Control serves the following design purposes:

  • To decouple the execution of a task from implementation.
  • To focus a module on the task it is designed for.
  • To free modules from assumptions about how other systems do what they do and instead rely on contracts.
  • To prevent side effects when replacing a module.

一。在

The goal of the dependency inversion principle is to decouple application glue code from application logic...

The principle states:

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.

这和我想用这个术语得到的一样具体,给定the disputed status of it's value。但正是这些类型的担忧激发了Martelli(evidenced by his 2007 talk especially)。在

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)

为了测试EnLolCat,我们可能发现自己在类中使用了粘合代码,并且可以很容易地测试一个自由函数,因为它没有任何副作用,而且是完全确定的。换句话说,我们正在做更多的事情。在

但是,当我们测试NewClass方法时,我们的情况不是一样的吗?Not necessarily。在

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.

依赖关系对测试中的代码是不好的,所以如果每个依赖项都使代码变得更难看一点也许是件好事。丑陋的成本比紧耦合的成本低,但开发者更可能认真对待这一点。在

最后说明

  • 即使是在紧急情况下。在
  • 除非他真的需要。在

相关问题 更多 >