最近小组里面引进了Spock这个测试框架,本人在实际使用了以后,体验很是不错,本篇文章一是为了巩固输入的知识,二是为了向你们推广一下。html
在了解学习Spock测试框架以前,咱们应该先关注单元测试自己,了解咱们常见的单测痛点,这样才能更好地去了解Spock这个测试框架是什么,咱们为何要使用它,能解决咱们什么痛点。java
如今让咱们开始吧。git
咱们写代码免不了要测试,测试有不少种,对于Javaer们来讲,最初级的测试是写个main函数运行一个函数结果,或者说把系统启起来本身模拟一下请求,看输入输出是否符合预期,更高级地,会用各类测试套件,测试系统。每一个测试都有它的关注点,好比测试功能是否正确,系统性能瓶颈等等。github
那咱们常说的单元测试呢?数据库
单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工做。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。express
-- 摘自维基百科编程
以上是维基百科的说明。缓存
单元测试固然不是必须之物,没了单测你的程序通过QA团队的端到端测试和集成测试以后,也能保证正确性。可是从另外的角度来看,单元测试也是必须之物。好比持续部署的前提之一就是有单元测试的保障,还有在重构代码的时候,没有单元测试你会步履维艰。bash
单元测试的好处包括但不限于:数据结构
提高软件质量
优质的单元测试能够保障开发质量和程序的鲁棒性。越早发现的缺陷,其修复的成本越低。
促进代码优化
单元测试的编写者和维护者都是开发工程师,在这个过程中开发人员会不断去审视本身的代码,从而(潜意识)去优化本身的代码。
提高研发效率
编写单元测试,表面上是占用了项目研发时间,可是在后续的联调、集成、回归测试阶段,单测覆盖率高的代码缺陷少、问题已修复,有助于提高总体的研发效率。
增长重构自信
代码的重构通常会涉及较为底层的改动,好比修改底层的数据结构等,上层服务常常会受到影响;在有单元测试的保障下,咱们对重构出来的代码会多一份底气。
宏观上,单元测试要符合 AIR 原则:
微观上,单元测试代码层面要符合 BCDE 原则:
下列痛点是平常开发中可能会遇到的,
对上面几点稍微作下解释。
首先,测试代码的代码量绝对不会比业务代码少(假设有覆盖率指标,且不做弊),有时候一个函数,输入和输出会有多种状况,想要彻底覆盖,代码量只会更多。较多的代码量,加上单测代码并不想业务代码那样直观(靠写注释的方式,看的乱,写的累),还有一部分编码人员对代码可读性不重视,最终就会致使单元测试的代码难以阅读,更难以维护。同时,大部分单元测试的框架都对代码有很强的侵入性,要想理解单元测试,首先得学习一下那个单元测试框架。从这个角度来看,维护的难度又增长了。
再说说,单元测试存在外部依赖的状况,也就是第1、二点,想要写一个纯粹的无依赖的单元测试每每很困难,好比依赖了数据库,依赖了其余模块,因此不少人在写单元测试时选择依赖一部分资源,好比在本机启动一个数据库。这类所谓的“单元测试”每每很流行,可是对于多人合做的项目,这类测试却常常容易形成混乱。 好比说要在本地读个文件,或者链接某个数据库,其余修改代码的人(或者持续集成系统中)并无这些东西,因此测试也都无法经过。最后大部分这类测试代码的下场都是用不了、也舍不得删,只好被注释掉,扔在那里。随着开源项目逐渐发展,对外部资源的依赖问题开始能够经过一些测试辅助工具解决,好比使用内存型数据库H2代替链接实际的测试数据库,不过能替代的资源类型始终有限。
而实际工做过程当中,还有一类难以处理的依赖问题:代码依赖。好比一个对象的方法中调用了其它对象的方法,其它对象又调用了更多对象,最后造成了一个无比巨大的调用树。后来出现了一些mock框架,好比java的JMockit、EasyMock,或者Mockito。利用这类框架能够相对比较轻松的经过mock方式去作假设和验证,相对于以前的方式有了质的飞跃。
可是,在这里须要强调一个观点,写单元测试的难易程度跟代码的质量关系最大,而且是决定性的。项目里不管用了哪一个测试框架都不能解决代码自己难以测试的问题。
简单来讲,有时候你以为你的代码很难写单元测试,说明代码写的不是很好,须要去关注代码的逻辑抽象设计是否合理,一步步去重构你的代码,让你的代码变得容易测试。但这些又属于代码重构方面的知识了,涉及到不少的设计原则。推荐阅读《重构-改善既有代码的设计》《修改代码的艺术》 《敏捷软件开发:原则、模式与实践》这几本著做。
不少开发人员对待单元测试,存在心态上的障碍,
那是测试同窗干的事情。(开发人员要作好单元测试
单元测试代码是多余的。 (汽车的总体功能与各单元部件的测试正常与否是强相关
单元测试代码不须要维护。 一年半载后,那么几乎处于废弃状态(单元测试代码是须要随着项目开发一直维护的
单元测试与线上故障没有辩证关系。(好的单元测试能最大限度规避线上故障
Spock能给你提供整个测试生命周期中可能须要的全部测试工具。它带有内置的模拟打桩,以及专门为集成测试建立的一些额外的测试注释。同时,因为Spock是较新的测试框架,所以它有时间观察现有框架的常见缺陷,并加以解决或提供更优雅的解决方法。
specification 来源于近期流行起来写的BDD(Behavior-driven development 行为驱动测试)。在TDD的基础上,经过测试来表达代码的行为。经过某种规范说明语言去描述程序“应该”作什么,再经过一个测试框架读取这些描述、并验证应用程序是否符合预期。把需求转化成Given/When/Then的三段式,因此你看到测试框架有这种Given/When/Then三段式语法的,通常来讲背后都是BDD思想,好比上图中的Cucumber和JBehave。
如今让咱们以最快速的方式,来使用一次Spock
建立一个空白项目:spock-example
,选择maven工程。
<dependencies>
<!-- Mandatory dependencies for using Spock -->
<!-- 使用Spock必须的依赖 -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<!-- Optional dependencies for using Spock -->
<!-- 选择性使用的Spock相关依赖 -->
<dependency> <!-- use a specific Groovy version rather than the one specified by spock-core -->
<!-- 不使用Spock-core中定义的Groovy版本,而是本身定义 -->
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.5.7</version>
<type>pom</type>
</dependency>
<dependency> <!-- enables mocking of classes (in addition to interfaces) -->
<!-- mock 接口和类时要用 -->
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes without default constructor (together with CGLIB) -->
<!-- mock 类要用 -->
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
<dependency> <!-- only required if Hamcrest matchers are used -->
<!-- Hamcrest 是一个用于编写匹配对象的框架,若是用到了Hamcrest matchers,须要加这个依赖 -->
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<!-- Dependencies used by examples in this project (not required for using Spock) -->
<!-- 使用h2base作测试数据库-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
<scope>test</scope>
</dependency>
</dependencies>
复制代码
<plugins>
<!-- Mandatory plugins for using Spock -->
<!--使用Spock的强制性插件 -->
<plugin>
<!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin,visit https://github.com/groovy/GMavenPlus/wiki -->
<!-- 这个 gmavenplus 插件是用于编译Groovy代码的 . 想获取更多此插件相关信息,visit https://github.com/groovy/GMavenPlus/wiki -->
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Optional plugins for using Spock -->
<!-- 选择性使用的Spock相关插件-->
<!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) -->
<!--只有当测试类不匹配默认的 Surefire patterns (`*Test` 等等.)-->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
<configuration>
<useFile>false</useFile>
<includes>
<include>**/*Test.java</include>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
...
</plugins>
复制代码
因为spock是基于groovy语言的,因此须要建立groovy的测试源码目录:首先在test目录下建立名为groovy的目录,以后将它设为测试源码目录。
/** * @author Richard_yyf * @version 1.0 2019/10/1 */
public class Calculator {
public int size(String str){
return str.length();
}
public int sum(int a, int b) {
return a + b;
}
}
复制代码
Ctrl + Shift + T
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Title
import spock.lang.Unroll
/** * * @author Richard_yyf * @version 1.0 2019/10/1 */
@Title("测试计算器类")
@Subject(Calculator)
class CalculatorSpec extends Specification {
def calculator = new Calculator()
void setup() {
}
void cleanup() {
}
def "should return the real size of the input string"() {
expect:
str.size() == length
where:
str | length
"Spock" | 5
"Kirk" | 4
"Scotty" | 6
}
// 测试不经过
def "should return a+b value"() {
expect:
calculator.sum(1,1) == 1
}
// 不建议用中文哦
@Unroll
def "返回值为输入值之和"() {
expect:
c == calculator.sum(a, b)
where:
a | b | c
1 | 2 | 3
2 | 3 | 5
10 | 2 | 12
}
}
复制代码
这里模拟一个缓存服务做为例子
/** * @author Richard_yyf * @version 1.0 2019/10/2 */
public interface CacheService {
String getUserName();
}
复制代码
public class Calculator {
private CacheService cacheService;
public Calculator(CacheService cacheService) {
this.cacheService = cacheService;
}
public boolean isLoggedInUser(String userName) {
return Objects.equals(userName, cacheService.getUserName());
}
...
}
复制代码
测试类
class CalculatorSpec extends Specification {
// mock对象
// CacheService cacheService = Mock()
def cacheService = Mock(CacheService)
def calculator void setup() {
calculator = new Calculator(cacheService)
}
def "is username equal to logged in username"() {
// stub 打桩
cacheService.getUserName(*_) >> "Richard"
when:
def result = calculator.isLoggedInUser("Richard")
then:
result
}
...
}
复制代码
运行测试
在Spock中,待测系统(system under test; SUT) 的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类须要继承自Specification类。命名遵循Java规范。
每一个测试方法能够直接用文本做为方法名,方法内部由given-when-then
的三段式块(block)组成。除此之外,还有and
、where
、expect
等几种不一样的块。
@Title("测试的标题")
@Narrative("""关于测试的大段文本描述""")
@Subject(Adder) //标明被测试的类是Adder
@Stepwise //当测试方法间存在依赖关系时,标明测试方法将严格按照其在源代码中声明的顺序执行
class TestCaseClass extends Specification {
@Shared //在测试方法之间共享的数据
SomeClass sharedObj def setupSpec() {
//TODO: 设置每一个测试类的环境
}
def setup() {
//TODO: 设置每一个测试方法的环境,每一个测试方法执行一次
}
@Ignore("忽略这个测试方法")
@Issue(["问题#23","问题#34"])
def "测试方法1" () {
given: "给定一个前置条件"
//TODO: code here
and: "其余前置条件"
expect: "随处可用的断言"
//TODO: code here
when: "当发生一个特定的事件"
//TODO: code here
and: "其余的触发条件"
then: "产生的后置结果"
//TODO: code here
and: "同时产生的其余结果"
where: "不是必需的测试数据"
input1 | input2 || output
... | ... || ...
}
@IgnoreRest //只测试这个方法,而忽略全部其余方法
@Timeout(value = 50, unit = TimeUnit.MILLISECONDS) // 设置测试方法的超时时间,默认单位为秒
def "测试方法2"() {
//TODO: code here
}
def cleanup() {
//TODO: 清理每一个测试方法的环境,每一个测试方法执行一次
}
def cleanupSepc() {
//TODO: 清理每一个测试类的环境
}
复制代码
是Spock规格(Specification)的核心,其描述了SUT应具有的各项行为。每一个Specification都会包含一组相关的Feature methods:
def "should return a+b value"() {
expect:
calculator.sum(1,1) == 1
}
复制代码
每一个feature method又被划分为不一样的block,不一样的block处于测试执行的不一样阶段,在测试运行时,各个block按照不一样的顺序和规则被执行,以下图:
Setup Blocks
setup
也能够写成given
,在这个block中会放置与这个测试函数相关的初始化程序,如:
def "is username equal to logged in username"() {
setup:
def str = "Richard"
// stub 打桩
cacheService.getUserName(*_) >> str
when:
def result = calculator.isLoggedInUser("Richard")
then:
result
}
复制代码
When and Then Blocks
when
与then
须要搭配使用,在when
中执行待测试的函数,在then
中判断是否符合预期
Expect Blocks
expect能够看作精简版的when+then,如
when:
def x = Math.max(1, 2)
then:
x == 2
复制代码
简化成
expect:
Math.max(1, 2) == 2
复制代码
条件相似junit中的assert,就像上面的例子,在then或expect中会默认assert全部返回值是boolean型的顶级语句。若是要在其它地方增长断言,须要显式增长assert关键字
若是要验证有没有抛出异常,能够用thrown()
def "peek"() {
when: stack.peek()
then: thrown(EmptyStackException)
}
复制代码
若是要验证没有抛出某种异常,能够用notThrown()
Mock 是描述规范下的对象与其协做者之间(强制)交互的行为。
1 * subscriber.receive("hello")
| | | |
| | | argument constraint
| | method constraint
| target constraint
cardinality
复制代码
def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
复制代码
class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
def setup() {
publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
publisher.subscribers << subscriber2
}
复制代码
1 * subscriber.receive("hello") // exactly one call
0 * subscriber.receive("hello") // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello") // any number of calls, including zero
// (rarely needed; see 'Strict Mocking')
复制代码
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object
复制代码
1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression (here: method name starts with 'r' and ends in 'e')
复制代码
1 * subscriber.receive("hello") // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello") // an argument that is unequal to the String "hello"
1 * subscriber.receive() // the empty argument list (would never match in our example)
1 * subscriber.receive(_) // any single argument (including null)
1 * subscriber.receive(*_) // any argument list (including the empty argument list)
1 * subscriber.receive(!null) // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive(endsWith("lo")) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 && it.contains('a') })
// an argument that satisfies the given predicate, meaning that
// code argument constraints need to return true of false
// depending on whether they match or not
// (here: message length is greater than 3 and contains the character a)
复制代码
Stubbing 是让协做者以某种方式响应方法调用的行为。在对方法进行存根化时,不关心该方法的调用次数,只是但愿它在被调用时返回一些值,或者执行一些反作用。
subscriber.receive(_) >> "ok"
| | | |
| | | response generator
| | argument constraint
| method constraint
target constraint
复制代码
如:subscriber.receive(_) >> "ok"
意味,无论什么实例,什么参数,调用 receive 方法皆返回字符串 ok
使用 >>
操做符,返回固定值
subscriber.receive(_) >> "ok"
复制代码
返回一个序列,迭代且依次返回指定值。以下所示,第一次调用返回 ok,第二次调用返回 error,以此类推
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
复制代码
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
复制代码
subscriber.receive(_) >> { throw new InternalError("ouch") }
复制代码
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
复制代码
本文介绍了单元测试的基础知识,和Spock的一些用法。使用Spock,能够享受到groovy脚本语言的方便、一站式的测试套件,写出来的测试代码也更加优雅、可读。
可是这只是第一步,学会了如何使用一个测试框架,只是初步学会了“术”而已,要如何利用好Spock,须要不少软性方面的改变,好比如何写好一个测试用例,如何渐进式地去重构代码和写出更易测试的代码,如何让团队实行TDD等等。
但愿能在之后分享更多相关的知识。