当无法避免做一件事时,那就让它变得更简单。

概述###

单测是规范的软件开发流程中的必不可少的环节之一。再伟大的程序员也难以避免自己不犯错,不写出有BUG的程序。单测就是用来检测BUG的。Java阵营中,JUnit和TestNG是两个知名的单测框架。不过,用Java写单测实在是很繁琐。本文介绍使用Groovy+Spock轻松写出更简洁的单测。

Spock是基于JUnit的单测框架,提供一些更好的语法,结合Groovy语言,可以写出更为简洁的单测。Spock介绍请自己去维基,本文不多言。下面给出一些示例来说明,如何用Groovy+Spock来编写单测。

准备与基础###

maven依赖####

要使用Groovy+Spock编写单测,首先引入如下Maven依赖,同时安装Groovy插件。

<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency> <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency> <dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.1-groovy-2.4</version>
<scope>test</scope>

基本构造块####

Spock主要提供了如下基本构造块:

  • where: 以表格的形式提供测试数据集合
  • when: 触发行为,比如调用指定方法或函数
  • then: 做出断言表达式
  • expect: 期望的行为,when-then的精简版
  • given: mock单测中指定mock数据
  • thrown: 如果在when方法中抛出了异常,则在这个子句中会捕获到异常并返回
  • def setup() {} :每个测试运行前的启动方法
  • def cleanup() {} : 每个测试运行后的清理方法
  • def setupSpec() {} : 第一个测试运行前的启动方法
  • def cleanupSpec() {} : 最后一个测试运行后的清理方法

了解基本构造块的用途后,可以组合它们来编写单测。

单测示例###

expect-where####

expect-where组合是最简单的单测模式。也就是在 where 子句中以表格形式给出一系列输入输出的值,然后在 expect 中引用,适用于不依赖外部的工具类函数。这里的Where子句类似于TestNG里的DataProvider,比之更简明。 如下代码给出了二分搜索的一个实现:

      /**
* 二分搜索的非递归版本: 在给定有序数组中查找给定的键值
* 前提条件: 数组必须有序, 即满足: A[0] <= A[1] <= ... <= A[n-1]
* @param arr 给定有序数组
* @param key 给定键值
* @return 如果查找成功,则返回键值在数组中的下标位置,否则,返回 -1.
*/
public static int search(int[] arr, int key) { int low = 0;
int high = arr.length-1;
while (low <= high) {
int mid = (low + high) / 2;
if (arr[mid] > key) {
high = mid - 1;
}
else if (arr[mid] == key) {
return mid;
}
else {
low = mid + 1;
}
}
return -1;
}

要验证这段代码是否OK,需要指定arr, key, 然后看Search输出的值是否是指定的数字 result。 Spock单测如下:

class BinarySearchTest extends Specification {

    def "testSearch"() {
expect:
BinarySearch.search(arr as int[], key) == result where:
arr | key | result
[] | 1 | -1
[1] | 1 | 0
[1] | 2 | -1
[3] | 2 | -1
[1, 2, 9] | 2 | 1
[1, 2, 9] | 9 | 2
[1, 2, 9] | 3 | -1
//null | 0 | -1
} }

单测类BinarySearchTest.groovy 继承了Specification ,从而可以使用Spock的一些魔法。expect: 块非常清晰地表达了要测试的内容,而where: 块则给出了每个指定条件值(arr,key)下应该有的输出 result。 注意到 where 中的变量arr, key, result 被 expect 的表达式引用了。是不是非常的清晰简单 ? 可以任意增加一条单测用例,只是加一行被竖线隔开的值。

注意到最后被注释的一行, null | 0 | -1 这个单测会失败,抛出异常,因为实现中没有对 arr 做判空检查,不够严谨。 这体现了写单测时的一大准则:务必测试空与临界情况。此外,给出的测试数据集覆盖了实现的每个分支,因此这个测试用例集合是充分的。

Unroll####

testSearch的测试用例都写在where子句里。有时,里面的某个测试用例失败了,却难以查到是哪个失败了。这时候,可以使用Unroll注解,该注解会将where子句的每个测试用例转化为一个 @Test 独立测试方法来执行,这样就很容易找到错误的用例。 方法名还可以更可读些。比如写成:

    @Unroll
def "testSearch(#key in #arr index=#result)"() {
expect:
BinarySearch.search(arr as int[], key) == result where:
arr | key | result
[] | 1 | -1
[1, 2, 9] | 9 | 2
[1, 2, 9] | 3 | 0
}

运行结果如下。 可以看到错误的测试用例单独作为一个子测试运行,且标识得更明显了。

typecast####

注意到expect中使用了 arr as int[] ,这是因为 groovy 默认将 [xxx,yyy,zzz] 形式转化为列表,必须强制类型转换成数组。 如果写成 BinarySearch.search(arr, key) == result 就会报如下错误:

Caused by: groovy.lang.MissingMethodException: No signature of method: static zzz.study.algorithm.search.BinarySearch.search() is applicable for argument types: (java.util.ArrayList, java.lang.Integer) values: [[1, 2, 9], 3]
Possible solutions: search([I, int), each(groovy.lang.Closure), recSearch([I, int)

类似的,还有Java的Function使用闭包时也要做强制类型转换。来看下面的代码:

  public static <T> void tryDo(T t, Consumer<T> func) {
try {
func.accept(t);
} catch (Exception e) {
throw new RuntimeException(e.getCause());
}
}

这里有个通用的 try-catch 块,捕获消费函数 func 抛出的异常。 使用 groovy 的闭包来传递给 func 时, 必须将闭包转换成 Consumer 类型。 单测代码如下:

def "testTryDo"() {
expect:
try {
CatchUtil.tryDo(1, { throw new IllegalArgumentException(it.toString())} as Consumer)
Assert.fail("NOT THROW EXCEPTION")
} catch (Exception ex) {
ex.class.name == "java.lang.RuntimeException"
ex.cause.class.name == "java.lang.IllegalArgumentException"
}
}

这里有三个注意事项:

  1. 无论多么简单的测试,至少要有一个 expect: 块 或 when-then 块 (别漏了在测试代码前加个 expect: 标签), 否则 Spock 会报 “No Test Found” 的错误;
  2. Groovy闭包 { x -> doWith(x) } 必须转成 java.util.[Function|Consumer|BiFunction|BiConsumer|...]
  3. 若要测试抛出异常,Assert.fail("NOT THROW EXCEPTION") 这句是必须的,否则单测可以不抛出异常照样通过,达不到测试异常的目的。

when-then-thrown####

上面的单测写得有点难看,可以使用Spock的thrown子句写得更简明一些。如下所示: 在 when 子句中调用了会抛出异常的方法,而在 then 子句中,使用 thrown 接收方法抛出的异常,并赋给指定的变量 ex, 之后就可以对 ex 进行断言了。

def "testTryDoWithThrown"() {
when:
CatchUtil.tryDo(1, { throw new IllegalArgumentException(it.toString())} as Consumer) then:
def ex = thrown(Exception)
ex.class.name == "java.lang.RuntimeException"
ex.cause.class.name == "java.lang.IllegalArgumentException"
}

setup-given-when-then-where####

Mock外部依赖的单测一直是传统单测的一个头疼点。使用过Mock框架的同学知道,为了Mock一个服务类,必须小心翼翼地把整个应用的所有服务类都Mock好,并通过Spring配置文件注册好。一旦有某个服务类的依赖有变动,就不得不去排查相应的依赖,往往单测还没怎么写,一个小时就过去了。

Spock允许你只Mock需要的服务类。假设要测试的类为 S,它依赖类 D 提供的服务 m 方法。 使用Spock做单测Mock可以分为如下步骤:

STEP1: 可以通过 Mock(D) 来得到一个类D的Mock实例 d;

STEP2:在 setup() 方法中将 d 设置为 S 要使用的实例;

STEP3:在 given 子句中,给出 m 方法的模拟返回数据 sdata;

STEP4: 在 when 子句中,调用 D 的 m 方法,使用 >> 将输出指向 sdata ;

STEP5: 在 then 子句中,给出判定表达式,其中判定表达式可以引用 where 子句的变量。

例如,下面是一个 HTTP 调用类的实现。

package zzz.study.tech.batchcall;

import com.alibaba.fastjson.JSONObject;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import java.nio.charset.Charset; /**
* Created by shuqin on 18/3/12.
*/
@Component("httpClient")
public class HttpClient { private static Logger logger = LoggerFactory.getLogger(HttpClient.class); private CloseableHttpClient syncHttpClient = SyncHttpClientFactory.getInstance(); /**
* 发送查询请求获取结果
*/
public JSONObject query(String query, String url) throws Exception {
StringEntity entity = new StringEntity(query, "utf-8");
HttpPost post = new HttpPost(url);
Header header = new BasicHeader("Content-Type", "application/json");
post.setEntity(entity);
post.setHeader(header); CloseableHttpResponse resp = null;
JSONObject rs = null;
try {
resp = syncHttpClient.execute(post);
int code = resp.getStatusLine().getStatusCode();
HttpEntity respEntity = resp.getEntity();
String response = EntityUtils.toString(respEntity, Charset.forName("utf-8")); if (code != 200) {
logger.warn("request failed resp:{}", response);
}
rs = JSONObject.parseObject(response);
} finally {
if (resp != null) {
resp.close();
}
}
return rs;
} }

它的单测类如下所示:

package zzz.study.batchcall

import com.alibaba.fastjson.JSON
import org.apache.http.ProtocolVersion
import org.apache.http.entity.BasicHttpEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.execchain.HttpResponseProxy
import org.apache.http.message.BasicHttpResponse
import org.apache.http.message.BasicStatusLine
import spock.lang.Specification
import zzz.study.tech.batchcall.HttpClient /**
* Created by shuqin on 18/3/12.
*/
class HttpClientTest extends Specification { HttpClient httpClient = new HttpClient()
CloseableHttpClient syncHttpClient = Mock(CloseableHttpClient) def setup() {
httpClient.syncHttpClient = syncHttpClient
} def "testHttpClientQuery"() { given:
def statusLine = new BasicStatusLine(new ProtocolVersion("Http", 1, 1), 200, "")
def resp = new HttpResponseProxy(new BasicHttpResponse(statusLine), null)
resp.statusCode = 200 def httpEntity = new BasicHttpEntity()
def respContent = JSON.toJSONString([
"code": 200, "message": "success", "total": 1200
])
httpEntity.content = new ByteArrayInputStream(respContent.getBytes("utf-8"))
resp.entity = httpEntity when:
syncHttpClient.execute(_) >> resp then:
def callResp = httpClient.query("query", "http://127.0.0.1:80/xxx/yyy/zzz/list")
callResp.size() == 3
callResp[field] == value where:
field | value
"code" | 200
"message" | "success"
"total" | 1200 }
}

让我来逐一讲解:

STEP1: 首先梳理依赖关系。 HttpClient 依赖 CloseableHttpClient 实例来查询数据,并对返回的数据做处理 ;

STEP2: 创建一个 HttpClient 实例 httpClient 以及一个 CloseableHttpClient mock 实例: CloseableHttpClient syncHttpClient = Mock(CloseableHttpClient) ;

STEP3: 在 setup 启动方法中,将 syncHttpClient 设置给 httpClient ;

STEP4: 从代码中可以知道,httpClient 依赖 syncHttpClient 的 execute 方法返回的 CloseableHttpResponse 实例,因此,需要在 given: 块中构造一个 CloseableHttpResponse 实例 resp 。这里费了一点劲,需要深入apacheHttp源代码,了解 CloseableHttpResponse 的继承实现关系, 来最小化地创建一个 CloseableHttpResponse 实例 ,避开不必要的细节。不过这并不是 SpockMock单测的重点。

STEP5:在 when 块中调用 syncHttpClient.execute(_) >> resp ;

STEP6: 在 then 块中根据 resp 编写断言表达式,这里 where 是可选的。

嗯,Spock Mock 单测就是这样:setup-given-when-then 四步曲。读者可以打断点观察单测的单步运行。

小结###

本文讲解了使用Groovy+Spock编写单测的 expect-where , when-then-thrown, setup-given-when-then[-where] 三种最常见的模式,相信已经可以应对实际应用的大多数场景了。 可以看到,Groovy 的语法结合Spock的魔法,确实让单测更加清晰简明。

使用Groovy+Spock轻松写出更简洁的单测的更多相关文章

  1. [label][翻译][JavaScript-Translation]七个步骤让你写出更好的JavaScript代码

    7 steps to better JavaScript 原文链接: http://www.creativebloq.com/netmag/7-steps-better-javascript-5141 ...

  2. 如何在 ASP.NET Core 中写出更干净的 Controller

    你可以遵循一些最佳实践来写出更干净的 Controller,一般我们称这种方法写出来的 Controller 为瘦Controller,瘦 Controller 的好处在于拥有更少的代码,更加单一的职 ...

  3. 使用Java函数接口及lambda表达式隔离和模拟外部依赖更容易滴单测

    概述 单测是提升软件质量的有力手段.然而,由于编程语言上的支持不力,以及一些不好的编程习惯,导致编写单测很困难. 最容易理解最容易编写的单测,莫过于独立函数的单测.所谓独立函数,就是只依赖于传入的参数 ...

  4. 《数据结构与算法之美》 <05>链表(下):如何轻松写出正确的链表代码?

    想要写好链表代码并不是容易的事儿,尤其是那些复杂的链表操作,比如链表反转.有序链表合并等,写的时候非常容易出错.从我上百场面试的经验来看,能把“链表反转”这几行代码写对的人不足 10%. 为什么链表代 ...

  5. 在用 JavaScript 工作时,我们经常和条件语句打交道,这里有5条让你写出更好/干净的条件语句的建议。

    1.多重判断时使用 Array.includes 2.更少的嵌套,尽早 return 3.使用默认参数和解构 4.倾向于遍历对象而不是 Switch 语句 5.对 所有/部分 判断使用 Array.e ...

  6. 9条消除if...else的锦囊妙计,助你写出更优雅的代码

    前言 最近在做代码重构,发现了很多代码的烂味道.其他的不多说,今天主要说说那些又臭又长的if...else要如何重构. 在介绍更更优雅的编程之前,让我们一起回顾一下,不好的if...else代码 一. ...

  7. 写出更好的 JavaScript 条件语句

    1. 使用 Array.includes 来处理多重条件 // 条件语句 function test(fruit) { if (fruit == 'apple' || fruit == 'strawb ...

  8. 让我们一起写出更有效的CSharp代码吧,少年们!

    周末空闲,选读了一下一本很不错的C#语言使用的书,特此记载下便于对项目代码进行重构和优化时查看. Standing On Shoulders of Giants,附上思维导图,其中标记的颜色越深表示在 ...

  9. Java 11正式发布,这几个逆天新特性教你写出更牛逼的代码

    就在前段时间,Oracle 官方宣布 Java 11 (18.9 LTS) 正式发布,可在生产环境中使用! 这无疑对我们来说是一大好的消息.作为一名java开发者来说,虽然又要去学习和了解java11 ...

随机推荐

  1. python面向对象的三大特性

    一.继承 面向对象中的继承就是继承的类直接拥有被继承类的属性而不需要在自己的类体中重新再写一遍,其中被继承的类叫做父类.基类,继承的类叫做派生类.子类.在python3中如果不指定继承哪个类,默认就会 ...

  2. SpringBoot-@value自定义参数

    自定义参数 配置文件值 name=itmayiedu.com 代码:       @Value("${name}")       private String name; @Res ...

  3. MySQL中varchar最大长度是多少?

    一. varchar存储规则: 4.0版本以下,varchar(20),指的是20字节,如果存放UTF8汉字时,只能存6个(每个汉字3字节) 5.0版本以上,varchar(20),指的是20字符,无 ...

  4. 解决无法连接到 reCAPTCHA 服务

    今天ytkah在查询一个信息时需要人机验证,但提示“无法连接到 reCAPTCHA 服务”,通过修改host文件可以解决相关问题,用editplus或notepad打开C:\Windows\Syste ...

  5. Laravel展示产品-CRUD之show

    上一篇讲了Laravel创建产品-CRUD之Create and Store,现在我们来做产品展示模块,用到是show,①首先我们先修改controller,文件是在/app/Http/Control ...

  6. RN无限轮播以及ScrollView的大小调节问题

    如果你的ScrollView的大小是全屏,height不能用,这种情况需要给ScrollView添加一个容器View,然后调节容器View的大小 无限轮播这里我使用的是一个第三方的插件react-na ...

  7. Cartographer源码阅读(1):程序入口

    带着几个思考问题: (1)IMU数据的使用,如何融合,Kalman滤波? (2)图优化的具体实现,闭环检测的策略? (3)3D激光的接入和闭环策略? 1. 安装Kdevelop工具: http://b ...

  8. syslog-ng应用详解

    syslog-ng应用详解   科技小能手 2017-11-07 02:43:00 浏览136 评论0 日志 LOG 配置 主机 syslog source file varchar 摘要: 最近做一 ...

  9. 深入理解Lua的闭包一:概念、应用和实现原理

    本文首先通过具体的例子讲解了Lua中闭包的概念,然后总结了闭包的应用场合,最后探讨了Lua中闭包的实现原理.   闭包的概念 在Lua中,闭包(closure)是由一个函数和该函数会访问到的非局部变量 ...

  10. Java写xml文件

    import java.io.FileOutputStream; import org.dom4j.Document; import org.dom4j.DocumentHelper; import ...