回顾python中的单元测试和模拟

发表于:2017-10-20 14:45

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:南北    来源:51Testing软件测试网采编

  这篇文章涵盖了在我过去一年半的Python测试经验中证明有效的更高级的软件工程原理。特别地,我想重新思考patching单元测试中的mock对象。
  patching外部客户端
  本篇文章中的客户端指的是任何产生副作用的对象,副作用例如磁盘写入或者网络I/O。想象一个类,CloudCreator,从HTTP接受信息,产生了一些副作用,如创建了一些云基础设施,并通过HTTP在响应中发送信息:
  我们可以像下面这样测试CloudCreator:
  patch给了我们测试我们的CloudCreator类而不产生任何网络副作用的能力。但是这种设计有一些缺陷。如果CloudCreator使用了大量外部客户端,我们需要堆栈大量patch调用。此外,CloudCreator和它的单元测试极其依赖于HTTPClient;这使得更改网络客户端变得困难。
  Fugue工程师JoshEinhorn指出patch的一些其他缺点:
  ●使用它意味着在类中某处存在隐式依赖关系,而另一位开发人员不知道这一点。构造函数args使依赖显式。
  ●当重构底层实现时,使用patch会需要更新多个不相关的单元测试,而且由于patch使用硬编码字符串而不是更多强类型引用(可以由linters/IDEs识别),我们并不总能弄清哪些单元测试需要更改。
  ●使用patch是一个代码异味,因为这意味着被测试的类已经被耦合到一个或多个具体的类。
  ●依赖于patch来进行单元测试的代码不能移植到其他语言。带有编译器的静态类型语言不会允许猴子补丁(不严肃的运行)。这种结构的重构需要正确的单元测试,例如一个另一种语言的类。
  大体上,如果测试一个类需要大量patching外部客户端,这就是需要重构的标志。有经验的软件工程师将会知道这个例子是依赖反转的主要机会。
  依赖反转和注入
  在这种情况下,依赖反转,特别是依赖注入在软件工程领域中是老生常谈的主题。对于外行来说,依赖注入是这样的一个概念:一个类或函数应该被给与它所依赖的外部客户端,而不是自己去创建它。这使得代码可以在多个上下关系中运行,具体取决于它被给予的是什么客户端。
  在我们的例子中,CloudCreator创建云基础设施的核心功能不依赖于任何发送和接受消息的特定方式。因此,用这样的方式来写类是合乎逻辑的:网络I/O由在运行时注入的客户端处理,而不是硬编码(该代码使用Python的type-hinting语法):
  该封装允许类与HTTP客户端,TCP/IP客户端,ZMQ客户端,或者SQS/SNS客户端一起使用,只要网络I/O符合NetworkClient中定义的预设接口,例如NetworkClient.recv()和NetworkClient.send(data)。这是一个很简化的处理,精明的观察者会注意到,客户端的接口规范会变得至关重要,但那就是另一篇文章要说的了。
  依赖注入的一个主要优点是,它允许开发者在单元测试时轻松传入mock对象。现在我们可以将我们的测试升级成这样:
  我们创建一个mock网络客户端来进行单元测试,使用MagicMock的autospec参数来创建一个遵守NetworkClient接口的mock对象。
  在这个简单例子中,patch允许我们创建一个更死板的单元测试来遮盖一个死板的设计。正如我之前所说,如果你patching了不只几个调用,那这是你需要重构代码的标志了。请注意,patch依然有用,例如patchtime.time()调用或者其他无副作用库调用。
  更多依赖注入
  依赖注入非常有用,但当我们的类需要大量的客户端时我们该怎么办呢?我们可以添加更多的参数并分别注入他们:
  通过添加默认值,可以选择性地初始化客户端,这使得单元测试更加容易(这部分稍后再说)。但是,这种形式依旧是很笨重的,特别是在进行集成测试的时候。考虑一下,如果我们更改了我们的消息处理程序,并且之后想要测试它与服务端的通信是否正确,这会发生什么情况。初始化CloudCreator需要大量繁琐的工作来创建和初始化客户端对象。Python的优势之一就是其交互式解释器,它能够进行迭代开发过程,并保留了轻松使用REPL的能力,这使得开发者轻松了许多。要求开发人员创建并初始化十数个客户端对象,然后才能测试核心类的一个小更改,这会产生不小的挫败感。
  一个解决方法是将创建客户端封装到一个单独的对象中。我们甚至可以在构造函数中写入关于操作顺序和依赖关系的信息:
  请注意,CloudCreator仍然通过对所需服务的显式调用来进行初始化。这使得未来的开发人员轻松的了解CloudCreator需要什么服务。这使CloudCreator的构造函数只需要一个CloudCreatorServices对象就够了:
  然而,这将CloudCreator和CloudCreatorServices的具体实现联系起来,正好是CloudCreator需要的服务。如果将CloudCreator推广到为多个类创建服务,则调用者必须假定每一个单独的服务都是被使用推广的Service类的类需要的。
  不幸的是,这个天真的实现失去了原始依赖注入的灵活性。这很容易补救:
  在这一点上,所有我们所做的都是移动复杂的部分。开发者依旧负责初始化所有客户端。我们没有提供任何工具使他们工作得轻松一点。我们可以通过在CloudCreatorServices中建立复杂的初始化程序来做到这一点:
  现在我们把客户端的创建隐藏到这些初始化方法中了。这看起来是一个好的解决方法,但如果我们看得更仔细一点,这依然有一个缺点。当CloudCreatorServices被初始化的时候,我们创建了每一个客户端,即使我们知道我们将不会使用它。如果其中一个客户端服务失常并超时,但我们仍然想要测试其他功能时,我们该怎么办呢?有更灵活的办法吗?
  我们可以使用getter方法来改变初始化不变量的顺序:
  这个解决方案让我们可以延迟加载,所以客户端只按需要初始化,同时保持根据需要交换客户端的能力。但是,getter方法并不非常pythonic。有没有一个语言特性我们可以用来找到更Pythonic的方法呢?
  @property
  我们要找的这个特性就是@property:
  这看起来跟我们之前使用getter方法的解决方案很像,但我们去掉了get_,加入了@property装饰器。@property将getter方法转换成属性。作为一个属性就可以直接访问,就像CloudCreatorServices.database_client,不需要括号。此外,通过使用@<property_name>.setter将setter函数装饰成一个属性,可以让我们在将来添加一个setter。例如:
  当我们给database_client分配一个值时,setter将被透明地调用:
  使用@property可以保留直接访问实例属性的Python标准,同时给了我们在getter和setter中包含属性访问方法的灵活性。
  通过依赖注入进行mocking
  依赖反转的一个优点是使得单元测试更加简单。回想一下,CloudCreator的初始化参数具有默认值,这使我们可以选择性地模拟客户端服务对象来进行特殊测试:
  由于其他客户端服务没有被初始化,很简单可以判断出网络代码路径是否触及其范围之外的对象,这常常是一个某些地方不正常的信号。当然,全面的单元测试需要mock每一个服务,但这些都很容易添加。
  通过使用依赖反转,我们淘汰了在单元测试中patch的需要,同时为开发者提供了一个强大的,节省时间的集成测试工具。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号