译者的话
《JUnit实战》开篇第一句话说,所有的代码都需要进行测试。
近期准备在团队内部做一次关于单元测试的分享,为了准备这次分享,我把最近探索的一些单元测试的理念和具体方法做了下总结。我发现InfoQ上的这篇文章 stubbing-mocking-service-virtualization-differences 对我的影响比较大。之前写过两篇文章也是因为受到这篇文章的影响而写就的
我发现,以前我都只是使用上的一些实践,比如怎么使用 JUnit 做简单测试、参数化测试、如何Mock这类的问题,但是看了这篇文章让我更全面地了解了类似打桩、Mock和服务虚拟化这些具体是什么,适合哪些场景等。想推荐这篇文章给大家一起学习,但是找了一下并没有找到相关的译文,于是趁着周末将它翻译了一下。
本人水平有限,第一次翻译这么长的文章,而且文章里有一些表格比较费时,实话说,翻译到最后差点没坚持下来。还有一个问题是 stub、mock 和 service virtualization这几个词没有找到准确的中文翻译,artifact这个词翻译成工件,我其实很不能接受,但是确实找不到更合适的词了。如有差错请多指正。
————以下是正文————
前言
服务虚拟化(Service virtualization)是一项将测试套件从他的环境依赖中解耦出来的技术,它正在变得越来越受欢迎。广义上来说,它是包括模拟(mock)和打桩(stub)在内的“测试替身”中的一员。有很多工具支持服务虚拟化或者两种特别类似的方法: 打桩和模拟。本文对这三种方法及其相对优势进行了比较,并探讨了一些实际情况,以推荐哪种技术最有好处。目的是提供对所有这三种技术的理解,使你能够根据具体情况在模拟、打桩或虚拟服务之间做出最佳选择。
何时何地使用Stub、Mock和服务虚拟化?
让我们看一下三个你可能会遇到的场景。
第一个场景是,测试团队正在手动对系统进行测试(图1)。这个系统连接到后台系统中,而这个系统正在被很多团队一起用来测试。由于有非常多会变动的部件,因此测试变得非常脆弱。后端系统部署的环境经常会因为环境和部署的问题挂掉。在后台系统配置测试数据可能花费几天或者几个星期,而且容易出错。测试团队经常要等待后台系统或者测试数据可用才能进行测试。如果你是一个测试者,你经常会感到挫败,因为你不能高效地完成你的工作。如果你是一个管理者,你也会感觉沮丧,因为团队花了相当多的时间在获取外部依赖。更糟糕的是,你可能要花很多钱去配置测试数据,并为外部第三方API的测试事务买单。
一种解决这个太多依赖和变动的部件问题的办法是在与外部后台系统做集成测试做系统测试之前先做系统测试(图2)。使用虚拟服务或打桩,让你可以将测试从真实的后台系统中解耦出来。上面提到的问题都迎刃而解或者变得不那么重要了。人们因此可以高兴一会儿了。
最终,你可以会在测试过程中发现其他的瓶颈。如果你是一个测试人员,那么你希望开始编写自动化测试,并且自动化执行重复性的日常活动。这可以让你专注于那些你所擅长的,创造性和批判性地思考如何测试这个系统。作为一个管理者,你鼓励你的测试人员去探索其他测试系统的方法并且深入研究自动化测试。你决定试一试一个 BDD 框架,像 Cucumber,并且开始使用 “over the wire”(远程定位和运行)打桩和模拟。
不幸的是,开发人员和测试人员在沟通上有些问题,他们似乎在使用相同的词,但是并没有达成共识。此外,最近加入的顾问们宣称服务虚拟化是一种优于打桩和模拟的方式。当开发人员口中所说的 stub 和 mock 是什么意思呢?主我们来看一下打桩、模拟和虚拟服务之间的差异,究竟是什么因素导致了沟通上的问题。
测试替身(test double)是什么?
测试替身是一个在测试中替代真实对象的对象。测试替身允许我们在测试被测系统(SUT)的时候将应用程序和它的依赖项分离。将依赖替换成等效的接口,这个接口允许你执行特定的测试用例。
开发人员使用的常见测试替身分类有:
- 哑元对象(dummy object)(字符串 “Mike”)
- 桩(stub)(StubUserRepository 类,它永远返回一个居住在美国名叫约翰的32岁男性用户对象)
- 间谍(spy)(SpyHttpResponse 类,它记录所有 POST 方法的调用)
- 仿冒对象(fake)(FakeDatabase 类,它持久化到一个内存数据库H2而不是生产环境下的DB2数据库)
- 仿制对象(mock)(由 Mockito 生成的用于单元测试的 OrderObserver 接口的动态代理实现)
用于测试和质量保证(QA)的测试替身常见分类:
- 桩(stub)(使用 SoapUI 创建并布署到远程 Jetty 实例上的 war 文件中的 servlet)
- 虚拟服务(通过服务虚拟化工具创建并部署到一个远程共享服务环境的 artifact)
Mocks, stubs 和虚拟服务
最常讨论的测试替身的类型是 mocks、桩和虚拟服务(virtual services)。
桩:一个接口的最小实现,这个实现正常情况下返回硬编码的数据,它跟测试套件紧紧地耦合在一起。当测试套件非常简单而且在桩里面硬编码数据不是问题的时候,这种方式非常有用。一些桩是手写的,有些可以通过工具生成。一个桩通常由开发人员编写以备个人使用。它可以分享给测试人员,但更广泛地共享通常受到硬编码的软件平台和布署基础设施依赖性相关的协同问题的限制。一种常见的实战是,当一个桩直接跟类、方法、单元测试方法、模块和验收测试在同一进程中的情况。一些开发人中会说,桩也可以被启动,但是你无法验证对桩的调用。桩也可以用于远程调用,例如 HTTP,但是其他人可能会坚决主张对于这种测试用例应该使用虚拟服务。
Mock:一个可编程的接口观察者,它根据测试定义的期望对输出进行校验。它常常由第三方库生成。例如,Java 中的 Mockito, JMock 或者 WireMock等库。当你有一个巨大的测试套件时,打桩将不足以完成测试任务,因为每个测试用例需要不同的数据,在桩里面设置和维护这些数据将变得非常昂贵,这种情况下 Mock 就非常有用了。Mock 通常由开发人员编写以备个人使用,但是它也可以被分享给测试人员。然而更广泛地共享通常受到硬编码的软件平台和布署基础设施依赖性相关的协同问题的限制。它们通用被用于直接跟类、方法、单元测试方法、模块和验收测试在同一进程中的情况。Mock 对满足预定义条件的指定请求提供响应(也叫做请求或参数校验)。一个 mock 也关注交互而不是状态,因此 mock 通常是有状态的。举例来说,你可以校验给定的方法被调用了多少次,或者一个给定的对象被调用的顺序。
虚拟服务:一个测试替身,经常以软件即服务(SaaS)的形式提供,总是远程调用,而且从来不会跟方法在同一个进程中。虚拟服务通常使用某个服务虚拟化平台录制流量来创建而不是基于接口或者API文档从头开始搭建的交互模式。虚拟服务可以用于为团队建立一个通用的基础平台,以便与其他开发团队和测试团队交流和共享成果。虚拟服务通过 HTTP、TCP等进行远程调用,通常支持多种协议,例如 HTTP、MQ、TCP等,而打桩和Mock通常只支持其中一种。有时候虚拟服务会要求用户授权,特别是当布署在企业间可见的环境中。服务虚拟化工具被用于创建虚拟服务通常会有用户界面,在具体深入了解某个具体的协议是如何工作的之前,这些界面让即使不太懂技术的软件测试人员也可以把平台运转起来。这些服务通常会依赖一个数据库。它们也可以模拟系统的非方法特性,例如响应时间或者慢连接。你有时候会发现一些虚拟服务,这些服务为一部分为符合条件的请求提供打桩的响应,但将另一部分请求传递到实时后端系统的虚拟服务(部分打桩)。与 mock 类似,虚拟服务具有一些颇为复杂的请求匹配器,这些匹配器允许为很多不同类型的请求返回某个响应。有时候,虚拟服务通过构建基于请求的属性和数据的部分响应以模拟系统的行为。
很难绝对地说以下哪种类型的测试替身最合适。它们应该被看做是一个光谱,而不是严格的定义。
所有以上提及的方法各有利弊。我们后面一起来看一下这些利弊。
数据来源 | 数据耦合性 | 调用校验 | 调用协议 | 创建人 | 使用者 | 状态 | GUI | 测试阶段 | |
---|---|---|---|---|---|---|---|---|---|
Stub | 硬编码或根据测试用例设置 | 跟测试套件数据紧耦合 | 无 | 通常在同一进程中(JVM, .NET, YARV等)。有时会基于IP,例如HTTP或者原生TCP协议 | 开发/有时测试 | 开发/有时测试 | 无 | 无 | 通常用于单元、集成、系统和验收测试 |
Mock | 根据测试用例设置 | 弹性地,跟测试套件数据可以紧耦合也可以松耦合 | 经常使用 | 通常在同一进程中(JVM, .NET, YARV等)。有时会基于IP,例如HTTP或者原生TCP协议 | 主要是开发 | 开发/有时测试 | 有 | 有时是命令行 | 通常是单元、集成、系统和验收测试 |
虚拟服务 | 录制的数据(可能是在录制之后手动更改)或者硬编码的数据。有时响应是基于请求数据的。 | 与测试套件紧耦合 | 在做测试的时候测试人员有时会查看虚拟服务日志 | 总是通过网络层。通常支持多种协议,例如HTTP、MQ、FIX等 | 主要是测试人员。最近在微服务架构下更多地是开发人员 | 主要是测试人员。最近在微服务架构下更多地是开发人员 | 有 | 有 | 通常是测试人员在系统测试时使用。在微服务架构中几乎所有的测试阶段。 |
什么是服务虚拟化?
这是一种创建虚拟服务并在团队内部和跨团队的开发人员和测试人员之间共享这些服务的实践。在相同产品上工作的开发人员和测试人员可以使用相同的虚拟服务工件或虚拟服务。另一个例子是跨大型企业的测试团队使用相同的虚拟服务工件。它能促进跨多部门的开发和测试团队之间的交流。它还试图通过在一个大型组织内由多个团队同时为相同的API创建 stub 来解决重复工作的问题,方法是在团队之间建立新的通信渠道。我们稍后将讨论权衡。
请记住,以下比较仅包含一般的准则。根据你的实际情况,解决方案可能有所不同。
Stub、mock 和服务虚拟化解决不同的问题。一些问题三者可以解决。而一些问题则只能使用 mock 和 stub。为了只突出几个最常见的问题,请看下面的比较。
主要优点 | 主要缺点 | 何时使用 | 何时不使用 | |
---|---|---|---|---|
Stubbing | 有可用的开源软件可用。而且网上也有大量的技术文档可供查找 | 测试与stub因为硬编码导致紧耦合 | 当测试数据不复杂的时候使用。如果测试数据很复杂则使用mock,以执行所有的测试用例。如果你希望学习如何使用stub,这是一个好主意。 | 通常要求有中等的技术背景。避免在有复杂测试数据的大型验收测试套件中使用硬编码数据的 stub |
Mocking | 很多开源软件工具可用,而且网上也有大量的技术文档可供查找 | 主要给开发人员使用的工具。开发人员通用不会使用 mock | 所有等级的测试都可以使用,但是要了解测试边界和被测系统。如果你想学习如何使用 mock,这永远是一个好想法。 | 通常需要坚实的技术背景 |
服务虚拟化 | 易于使用。全家桶式的解决方案。支持多种协议和很多工具。很好的测试工具。可以录制流量。一旦这个工具在公司内部建立起来后非常易于跨团队间的共享 | 这个工具很昂贵。另外,你需要将测试和数据在虚拟服务中连系起来,这部分工作跟打桩是一样的。而且引领市场的工具在一个共享的虚拟服务环境模式下运行,这造成了很多个人和团队之间的依赖。 | 大型问题。很多API需要模拟。人们今天就想开始使用它。模拟非函数性的需求,例如响应时长和慢连接。部分打桩。 | 中小型企业中的敏捷团队,他们使用敏捷的方式工作而且他们自己知道如何使用开源的或者特地开发的工具创建stub和mock,并且将它们共享给测试人员。大企业的解决方案通常只会干扰团队的敏捷性。在验收测试中要避免使用,它可能会造成过强的测试套件和虚拟服务之间的依赖性。 |
mock、stub和虚拟服务应用的例子
打桩、mock和虚拟服务可以用于解决不同的问题。这里列举了几种场景,其中每一种方法都可以考虑使用。这个列表演示了示例的用法,但是并不能详尽所有。
以下两个场景中打桩可能是一个好的方法:
如果你是一个后端开发人员,正在开发 一个小型的新应用,这个应该使用了第三方库以跟外部的API进行交互。这时你可以在你的代码中创建一个接口,以使你自己与第三方库进行解耦。在单元测试时,写一个桩,它返回一个硬编码的值。为端对端的测试,使用第三方库类实现这个接口。
如果你是一个测试人员,需要在一个与依赖隔离的环境中测试一个应用,那么你可以这个应用所依赖的 HTTP RESTful APIs 打一个桩。可以使用你的团队开发人员提供的内部工具。
下面这两个示例情况是比较适合使用 mock 的:
-
如果你是一个后端开发人员,正在开发一个相当大的的应用,而且你需要将任何 HTTP 接口依赖通通解耦出来,使用一个远程 mock 框架,像 WireMock 或者 MountedBank,然后在验收测试中准备好这些模型。
-
如果你是一个后端开发人员,使用的代码库具有上千个类单元测试,这时 Mockito 可以让你 mock 出任何测试所依赖的类。
最后,举例说明虚拟服务应该被首先考虑使用的场景:
-
如果你是一个开发人员,正在一个非常庞大且复杂的历史遗留项目中做开发,这个项目有非常多的依赖,而你当前的测试覆盖率非常低(小于 5%)–特别是如果这个系统的依赖在三分之二的时间内是不可用的时候。服务虚拟化工具让你可以在依赖可用的时候录制流量,之后你就可以使用虚拟服务来重放这些流量以进行测试了。当你和你的测试使用虚拟服务的时候,因依赖不可用导致的影响就会变得比较小了。这是一种短期的或者说战术上的方法,让你有时间为你的应用程序创建一套自动测试,这些测试将不使用记录好的数据而是已准备好的数据。实现的好坏取决于你在开发的系统的性质。根据具体情况,交付可能非常容易也可能近乎不可能做到。
-
如果你是一个前端开发人员,正在开发 一个公共的 SOAP(简单对象访问协议),像天气API,利用 Sandbox (getsandbox.com) 或 TrafficParrot (trafficparrot.com) 基于WSDL文件生成虚拟服务。在虚拟服务中配置测试数据以表示所有的测试用例。例如,温度低于 -60°C(这是永远不可能从真实的天气API返回的)。
-
如果 你是一个非功能性测试(Non-Functional Testing, NFT)的测试人员,负责测试一个大型银行应用的性能,使用 tcpdump 在生产环境中记录一整天的出入流量信息。然后使用服务虚拟化工具导入流出的流量信息并创建一个虚拟服务。你可以使用 tcpreplay 和 JMeter Proxy 导入流入的流量到 JMeter 中。在你的运行环境中使用 JMeter 运行一个测试套件,然后使用虚拟服务将其他的依赖进行解耦。安全起见,每周都重复录制进程。这可以非常容易实现,也可能非常困难,取决于你的具体负责的系统。
-
你在一个类似微服务架构下进行开发。你的部门雇用了 20 个开发人员和 10 个测试人员负责持续交付 70 个小的应用程序。它们一起组成一个产品。这些应用程序被每个指定的开发团队隔离开来被分别进行测试。你的持续集成系统被维护得非常好。大部分团队会编写消费者驱动的契约(Consumer-Driven Contract)测试或者采用类似的实践。你在部门内没有遇到多少集成造成的问题。不幸的是,外部部门的环境并不是敏捷的模式。那些你需要连接的 API 非常慢,经常会不可用,而且建立测试数据非常地困难。为了解决这个问题,每个需要与第三方交互的团队都建立了虚拟服务。一些团队称之为 stub,或者甚至是各种他们发明的名字。这些虚拟服务被特定应用程序的验收测试用作持续集成(CI)构建的一部分。它们接着被团队的测试人员用作人工探索性测试(manual exploratory testing)的一部分。然后,它们被当作主要集成构建的一部分进行布署。主要集成构建在给定的有界上下文中一起运行所有的应用程序。最后,一些虚拟服务也被性能测试团队用于全栈性能测试。
更深入挖掘的一些资源
-
可以从 GooS book 中找到很好的 stubs 和 mocks 的概述。
-
如果你是软件开发者,尝试使用 Mockito 来学习如何使用 mock。
-
如果你是软件测试人员,尝试使用任何开源的工具进行打桩。可以从 这里 找到一些工具。
-
值得一看的是,任何企业提供商,如CA、IBM或Parasoft,但正如 ThoughtWorks 在 2014年6月所观察到的,大多数创新都来自于为开源做出贡献的做作业者。
About the Author
关于作者
Wojciech Bulaty,WB软件顾问咨询公司高级软件工程师。Wojciech 在写敏捷、自动化、XP、TDD、BDD、结对编程和简洁代码方面积累了数十年的实践编码经验。WB软件顾问咨询公司曾为英国天空广播公司、Lloyds以及一些创业公司提供咨询。他们最近的产品是 Traffic Parrot,一个提供打桩、mock和服务虚拟化的工具。
参考文献
- http://blog.trafficparrot.com/2015/05/service-virtualization-and-stubbing.html
- http://download.microsoft.com/download/3/A/7/3A7FA450-1F33-41F7-9E6D-3AA95B5A6AEA/MSDNMagazineSeptember2007en-us.chm
- http://martinfowler.com/articles/consumerDrivenContracts.html
- http://martinfowler.com/articles/microservices.html
- http://martinfowler.com/articles/mocksArentStubs.html
- http://wiremock.org/
- http://www-01.ibm.com/software/rational/servicevirtualization/
- http://www.ca.com/us/devcenter/ca-service-virtualization.aspx
- http://www.jmock.org/
- http://www.mbtest.org/
- http://www.soapui.org/
- http://www.soapui.org/getting-started/mock-services.html
- http://xunitpatterns.com/Mock Object.html
- http://xunitpatterns.com/Mocks, Fakes, Stubs and Dummies.html
- http://xunitpatterns.com/SUT.html
- http://xunitpatterns.com/Test Double.html
- http://xunitpatterns.com/Test Stub.html
- https://en.wikipedia.org/wiki/Service_virtualization
- https://en.wikipedia.org/wiki/Service_virtualization#Tools_available_in_the_market
- https://en.wikipedia.org/wiki/Software_as_a_service
- https://github.com/mockito/mockito
- https://www.parasoft.com/solution/service-virtualization/
- https://www.thoughtworks.com/radar/platforms/big-enterprise-solutions