编辑推荐: |
本文来自于ImportNew,介绍了Spock的主要功能和使用方法,在这个过程中,spock可以让你从重复的编码、繁重的维护工作中解脱出来,让编写测试回归为一件有幸福感和成就感的事情。 |
|
1.摘要
最近一段时间接触到了spock这个可以用于java和groovy项目的单元测试框架,写了一段时间单测之后认为这个框架不错,值得写一篇文章推广一下。
2.关于单元测试
很多人一谈到单元测试就会想到xUnit框架。对于一些java新人来说,会用jUnit就是会写单元测试,高级点的会捣鼓一下testng,然后就认为自己掌握了单元测试。
而实际上,很多人不怎么会写单元测试,甚至不知道单元测试究竟是干什么的。写单元测试要比写代码要难上许多,而这里说的难度跟框架没什么关系。
所以,在开始介绍spock之前,需要先抛开框架,谈谈单元测试本身的事情。在理解了单元测试之后才能更清楚spock框架是什么,以及它否能够更优雅的解决你的问题。
2.1.1.单元测试是什么
写代码免不了要做测试,测试有很多种,对于java来说,最初级的就是写个main函数运行一下看看结果,高级的可以用各种高大上的复杂的测试系统。每种测试都有它的关注点,比如测试功能是不是正确,或者运行状态稳不稳定,或者能承受多少负载压力,等等。
那么所谓的单元测试是什么?这里直接引用维基百科上的词条说明:
单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
所以,我眼中的“合格的”单元测试需要满足几个条件:
1.测试的是一个代码单元内部的逻辑,而不是各模块之间的交互。
2.无依赖,不需要实际运行环境就可以测试代码。
3.运行效率高,可以随时执行。
2.1.2.单元测试的定位
了解了单元测试是什么之后,第二个问题就是:单元测试是用来做什么的?
很多人第一反应是“看看程序有没有问题”,或者“确保没有bug”。单元测试确实可以测试程序有没有问题,但是,从我个人编程的经验来看,大部分情况下只是使用单元测试来“看看程序有没有问题”的话,效率反而不如把程序运行起来直接查看结果。原因有两个:
1.单元测试要写额外的代码,而不写单元测试,直接运行程序也可以测试程序有没有问题。
2.即使通过了单元测试,程序在实际运行的时候仍然有可能出问题。
但是,很多时候直接启动程序测试会比较慢,所以一些同学为了解决这个问题,采用了一个折中的办法:只加载要测试的模块和它所有的依赖模块,比如在测试时只加载这个模块相关的spring的配置文件。这时所谓的单元测试实际上是用xUnit框架运行的集成测试,并没有体现“单元”的概念。
而关于“纯粹的单元测试”在介绍语言或者框架的书里很少被提起,反而是介绍重构或者敏捷开发的书里经常会看到各种各样的关于单元测试的介绍。
在这里我总结了一下几个比较常见的单元测试的几个典型场景:
1.开发前写单元测试,通过测试描述需求,由测试驱动开发。
2.在开发过程中及时得到反馈,提前发现问题。
3.应用于自动化构建或持续集成流程,对每次代码修改做回归测试。
4.作为重构的基础,验证重构是否可靠。
还有最重要的一点:编写单元测试的难易程度能够直接反应出代码的设计水平,能写出单元测试和写不出单元测试之间体现了编程能力上的巨大的鸿沟。无论是什么样的程序员,坚持编写一段时间的单元测试之后,都会明显感受到代码设计能力的巨大提升。
2.2.单元测试的痛点
对于新人来说,很容易在编写单元测试的时候遇到这几类问题:
2.2.1.单元测试的资料不够全
这里不够全是相对于“编码”来说的。介绍如何编码、如何使用某个框架的书茫茫多,但是与编码同样重要的介绍单元测试的书却不多,翻来覆去好的也不多,并且都有一定年头了。(如果有这方面的好的资料,请推荐给我,多谢)
很多关于编程的书籍中并没有深入介绍如何进行单元测试,或者仅仅介绍了最基础的assert、jUnit里怎么定义一个测试函数之类,就没有然后了,给人的感觉是这样:
2.2.2.单元测试难以理解和维护
测试代码不像普通的应用程序一样有着很明确的作为“值”的输入和输出。举个例子,假如一个普通的函数要做下面这件事情:
1.接收一个user对象作为参数
2.调用dao层的update方法更新用户属性
3.返回true/false结果
那么,只需要在函数中声明一个参数、做一次调用、返回一个布尔值就可以了。但如果要对这个函数做一个“纯粹的”单元测试,那么它的输入和输出会有很多情况,比如其中一个测试是这样:
1.假设调用dao层的update方法会返回true。
2.程序去调用service层的update方法。
3.验证一下service是不是也返回了true。
无论是用什么样的单元测试框架,最后写出来的单元测试代码量也比业务代码只多不少,我在写代码过程中的经验值是:要在不作弊的情况下维持比较高的单元测试覆盖率,要有三倍于业务代码的单测代码。
更多的代码量,加上单测代码并不像业务代码那样直观,还有对单测代码可读性不重视的坏习惯,导致最终呈现出来的单测代码难以阅读,要维护更是难上加难。
同时,大部分单元测试的框架都有很强的代码侵入性。要理解单元测试,首先得学习他用的那个单元测试框架,这无形中又增加了单元测试理解和维护的难度。
2.2.3.单元测试难以去除依赖
就像之前说的,如果要写一个纯粹的、无依赖的单元测试往往很困难,比如依赖了数据库、或者依赖了文件系统、再或者依赖了其它模块。
所以很多人在写单元测试时选择依赖一部分资源,比如在本机启动一个数据库。这类所谓的“单元测试”往往很流行,但是对于多人合作的项目,这类测试却经常容易造成混乱。
比如说要在本地读个文件,或者连接某个数据库,其他修改代码的人(或者持续集成系统中)并没有这些东西,所以测试也都没法通过。最后大部分这类测试代码的下场都是用不了、也舍不得删,只好被注释掉,扔在那里。
随着开源项目逐渐发展,对外部资源的依赖问题开始可以通过一些测试辅助工具解决,比如使用内存型数据库H2代替连接实际的测试数据库,不过能替代的资源类型始终有限。
而实际工作过程中,还有一类难以处理的依赖问题:代码依赖。比如一个对象的方法中调用了其它对象的方法,其它对象又调用了更多对象,最后形成了一个无比巨大的调用树。
很多比较旧的描述单元测试的书里写了一些传统的办法,这类方法基本上是先对耦合的部分做模拟,再对结果部分做断言。例如可以通过继承来自己做一个假的stub对象,最终用assert的方式验证正确性。但是这相当于对于每种假设都要做一个假的对象,而且对结果进行验证也比较复杂:比如我要验证“更新”操作是否真的调用了dao层,那么要自己在stub对象里对调用进行计数,验证时再对计数进行断言,非常繁琐。
后来出现了一些mock框架,比如java的JMockit、EasyMock,或者Mockito。利用这类框架可以相对比较轻松的通过mock方式去做假设和验证,相对于之前的方式有了质的飞跃,但是即使用上这类框架,遇到复杂的业务代码往往也无能为力。
而往往新人的代码质量往往不高,尤其是对代码的拆分和逻辑的抽象还处于懵懂阶段。要对这类代码写单测,即使是工作了3,4年的高级码农也是一个挑战,对新人来说几乎是不可能完成的任务。这也让很多新人有了“写单测很难”的感觉。
所以在这里需要强调一个观点,写单元测试的难易程度跟代码的质量关系最大,并且是决定性的。项目里无论用了哪个测试框架都不能解决代码本身难以测试的问题,所以如果你遇到的是“我的代码里依赖的东西太多了所以写不出来单测”这样的问题的话,需要去看的是如何设计和重构代码,而不是这篇文章。
2.3.推荐阅读
1.重构-改善既有代码的设计
2.修改代码的艺术
3.敏捷软件开发:原则、模式与实践
3.Spock是什么
3.1.简介
这里引用官方的介绍:
Spock
is a testing and specification framework for
Java and Groovy applications. What makes it
stand out from the crowd is its beautiful and
highly expressive specification language. Thanks
to its JUnit runner, Spock is compatible with
most IDEs, build tools, and continuous integration
servers. Spock is inspired from JUnit, jMock,
RSpec, Groovy, Scala, Vulcans, and other fascinating
life forms. |
简单地说,spock是一个测试框架,它的核心特性有以下几个:
1.可以应用于java或groovy应用的单元测试框架。
2.测试代码使用基于groovy语言扩展而成的规范说明语言(specification
language)。
3.通过junit runner调用测试,兼容绝大部分junit的运行场景(ide,构建工具,持续集成等)。
4.框架的设计思路参考了JUnit,jMock,RSpec,Groovy,Scala,Vulcans……
要理解spock的几个特性,还要理解几个关键名词:
3.1.1.groovy
引用维基百科上的介绍:
Groovy是Java平台上设计的面向对象编程语言。这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用。
Groovy的语法与Java非常相似,以至于多数的Java代码也是正确的Groovy代码。Groovy代码动态的被编译器转换成Java字节码。由于其运行在JVM上的特性,Groovy可以使用其他Java语言编写的库。
groovy是一门比较轻量,学习门槛也比较低的语言。对于只用过java语言的程序员来说,groovy是一个很不错的开拓视野的机会。如果你没有接触过groovy,那么可以参考这两条:
1.可以用纯java的语法写groovy。
2.参考这篇快速入门。
我个人比较喜欢groovy语言,在一些小项目中经常使用它。引用一下R大在知乎的回复:
Groovy比较讨好来自Java的程序员的一点是:用它写代码可以渐进的从接近Java的风格进化为接近Ruby的风格。使用接近Java风格写Groovy时,代码几乎跟Java一样,容易上手;而学习过程中可以逐渐用上各种类似Ruby的方便功能。
3.1.2.specification language
如果接触过不同语言类型的开源项目的话,就会发现有些项目中找不到测试目录(test),取而代之的是一个叫“spec”的目录,比如用ruby写的项目gitlab。这里的spec实际是specification的缩写,它的背后是一种近些年来开始流行起来的编程思想:BDD(Behavior-driven
development)。
关于BDD,同样是引用维基百科上的介绍:
BDD:行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD最初是由Dan
North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。
BDD的做法包括:
1.确立不同利益相关者要实现的远景目标
2.使用特性注入方法绘制出达到这些目标所需要的特性
3.通过由外及内的软件开发方法,把涉及到的利益相关者融入到实现的过程中
4.使用例子来描述应用程序的行为或代码的每个单元
5.通过自动运行这些例子,提供快速反馈,进行回归测试
6.使用“应当(should)”来描述软件的行为,以帮助阐明代码的职责,以及回答对该软件的功能性的质疑
7.使用“确保(ensure)”来描述软件的职责,以把代码本身的效用与其他单元(element)代码带来的边际效用中区分出来。
8.使用mock作为还未编写的相关代码模块的替身
BDD背后的编程思想超出了这篇文章的范围,这里就不再展开。上文说的specification language实际上是BDD其中一部分思想的实现手段:通过某种规范说明语言去描述程序“应该”做什么,再通过一个测试框架读取这些描述、并验证应用程序是否符合预期。
3.1.3.单元测试的运行场景
测试只有被执行之后才会有价值,这里就涉及一个“什么时候执行单元测试”的问题。
被接触最多的就是在IDE中执行单元测试,java程序员比较幸运,主流的java
IDE都可以很好的集成了单元测试功能,单元测试代码自动生成、测试覆盖率检查等功能也都成了IDE的标配。这些功能都能让程序员在编写代码的时候直接可以运行单元测试得到反馈。
其次,主流的构建工具(如maven、gradle)中也都实现了运行单元测试的功能,在生成二进制包之前可以对代码进行回归测试,这些构建工具都可以通过命令行调用,这是自动化构建的前提。
在此之上,依托于构建工具提供的自动化特性,在持续集成、持续部署的过程中可以执行自动化构建,在自动化构建的过程中通过构建工具执行单元测试,这是持续集成的流程中的重要步骤。
3.2.Spock与现有框架的对比
3.2.1.已有的java单元测试框架
就像刚才说的,有很多已有的单元测试框架,稍微老一点的如JMockit、EasyMock,新一点的类似Mockito和PowerMock。我之前一直在用testng+Mockito作为主要的单元测试框架,用它写过大概上万行单元测试,它的写法相对来说比较易读,功能也能满足大多数场景。
但在使用mockito的过程中也总是有一些不是很方便的地方,比如代码的可读性总还是差那么一点,比如像这样:
@Test public void testIsUserEnabled_userStatus
IsClosed_returnFalse()
throws Exception { UserInfo
userInfo = new
UserInfo(); userInfo.status = UserInfo.CLOSED;
doReturn(userInfo).when(userDao).getUserInfo(anyLong
()); boolean isUserEnabled
= userService.isUserEnabled(1l); Assert.assertFalse(isUserEnabled);
} |
虽然能读懂,但是对于它所做的事情全来说感觉说了很多废话,单元测试代码总是里充斥着各种when(),anyXXX(),return()之类啰嗦的关键词,加上java本身就是一个啰嗦的强类型的语言,这让写单测和读单测成为了一种体力活。
其次是单测数据,大部分测试都要提供数据,比如“当输入a的时候应该返回b”,如果只有一组数据那么没什么问题,但是当需要测试很多边界条件,需要多组数据的时候就会比较纠结。
用jUnit或者testng的dataprovider可以实现这个需求,但是无论是通过xml定义还是通过函数返回数据,都非常不方便。
最后,因为这些框架都只是一些独立的 |