image.png

0. 为什么人人都讨厌写单测

在之前的关于swagger文章里提到过,程序员最讨厌的两件事,一件是别人不写文档,另一件就是自己写文档。这里如果把文档换成单元测试也同样成立。
每个开发人员都明白单元测试的作用,也都知道代码覆盖率越高越好。高覆盖率的代码,相对来说出现 BUG 的概率就越低,在线上运行就越稳定,接的锅也就越少,就也不会害怕测试同事突然的关心。
既然这么多好处,为什么还会讨厌他呢?至少在我看来,单测有如下几点让我喜欢不起来的理由。
第一,要额外写很多很多的代码,一个高覆盖率的单测代码,往往比你要测试的,真正开发的业务代码要多,甚至是业务代码的好几倍。这让人觉得难以接受,你想想开发 5 分钟,单测 2 小时是什么样的心情。而且并不是单测写完就没事了,后面业务要是变更了,你所写的单测代码也要同步维护。
第二,即使你有那个耐心去写单测,但是在当前这个拼速度挤时间的大环境下,会给你那么多写单测的时间吗?写一个单测的时间可以实现一个需求,你会如何去选?
第三,写单测通常是一件很无趣的事,因为他比较死,主要目的就是为了验证,相比之下他更像是个体力活,没有真正写业务代码那种创造的成就感。写出来,验证不出bug很失落,白写了,验证出bug又感到自己是在打自己脸。

1. 为什么人人又必须写单测

所以得到的结论就是不写单测?那么问题又来了,出来混迟早是要还的,上线出了问题,最终责任人是谁?不是提需求的产品、不是没发现问题的测试同学,他们顶多就是连带责任。最该负责的肯定是写这段代码的你。特别是对于那些从事金融、交易、电商等息息相关业务的开发人员,跟每行代码打交通的都是真金白银。每次明星搞事,微博就挂,已经被传为笑谈,毕竟只是娱乐相关,如果挂的是支付宝、微信,那用户就没有那么大的包容度了。这些业务如果出现严重问题,轻则扫地出门,然后整个职业生涯背负这个污点,重则直接从面向对象开发变成面向监狱开发。所以单元测试保护的不仅仅是程序,更保护的是写程序的你
最后得出了一个无可奈何的结论,单测是个让人又爱又恨的东西,是不想做但又不得不做的事情。虽然我们没办法改变要写单测这件事,但是我们可以改变怎么去写单元测试这件事。

2. SPOCK 可以帮你改善单测体验

当然,本文不是教你用旁门左道的方法提高代码覆盖率。而是通过一个神奇的框架 spock 去提高你编写单元测试的效率。spock 这名称来源,个人猜测是因为《星际迷航》的同名人物(封面图)。那么spock 是如何提高编写单测的效率呢?我觉得有以下几点:
第一,他可以用更少的代码去实现单元测试,让你可以更加专注于去验证结果而不是写单测代码的过程。那么他又是如何做到少写代码这件事呢?原来他使用一种叫做 groovy 的魔法。
groovy 其实是一门基于 jvm 的动态语言。可以简单的理解成跑在 jvm 上的 python 或 js。说到这里,可能没有接触过动态语言的同学,对它们都会有一个比较刻板的印象,太过于灵活,很容易出现问题,且可维护性差,所以有了那一句『动态一时爽,全家 xxx』的梗。首先,这些的确是他的问题,严格的说是使用不当时才带来的问题。所以主要还是看使用的人。比如安卓领域的官方依赖管理工具 gradle 就是基于 groovy 开发的。
另外不要误以为我学这门框架,还要多学一门语言,成本太大。其实大可不必担心,你如果会 groovy 当然更好,如果不会也没有关系。因为 groovy 是基于 java 的,所以完全可以放心大胆的使用 java 的语法,某些要用到的 groovy 独有的语法很少,而且后面都会告诉你。
第二,他有更好的语义化,让你的单测代码可读性更高。
语义化这个词可能不太好理解。举两个例子来说吧,第一个是语义化比较好的语言 — HTML。他的语法特点就是标签,不同的类型放在不同的标签里。比如 head 就是头部的信息,body 是主体内容的信息,table 就是表格的信息,对于没有编程经验的人来说,也可以很容易理解。第二个是语义化比较差的语言 — 正则。他可以说基本上没有语义这种东西,由此导致的直接问题就是,即使是你自己的写的正则,几天之后你都不知道当时写的是什么。比如下面这个正则,你能猜出他是什么意思吗?(可以留言回复)

  1. ((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))

3. 领略 SPOCK 的魔法

3.1 引入依赖

  1. <!--如果没有使得 spring boot,以下包可以省略-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-test</artifactId>
  5. <scope>test</scope>
  6. </dependency>
  7. <!--引入spock 核心包-->
  8. <dependency>
  9. <groupId>org.spockframework</groupId>
  10. <artifactId>spock-core</artifactId>
  11. <version>1.3-groovy-2.5</version>
  12. <scope>test</scope>
  13. </dependency>
  14. <!--引入spock 与 spring 集成包-->
  15. <dependency>
  16. <groupId>org.spockframework</groupId>
  17. <artifactId>spock-spring</artifactId>
  18. <version>1.3-groovy-2.5</version>
  19. <scope>test</scope>
  20. </dependency>
  21. <!--引入 groovy 依赖-->
  22. <dependency>
  23. <groupId>org.codehaus.groovy</groupId>
  24. <artifactId>groovy-all</artifactId>
  25. <version>2.5.7</version>
  26. <scope>test</scope>
  27. </dependency>

说明

注释已经标明,第一个包是 spring boot 项目需要使用的,如果你只是想使用 spock,只要最下面 3 个即可。其中第一个包 spock-core 提供了 spock 的核心功能,第二个包 spock-spring 提供了与 spring 的集成(不用 spring 的情况下也可以不引入)。 注意这两个包的版本号 -> 1.3-groovy-2.5。第一个版本号 1.3 其实代表是 spock 的版本,第二个版本号代表的是 spock 所要依赖的 groovy 环境的版本。最后一个包就是我们要依赖的 groovy 。

3.2 准备基础测试类

3.2.1 Calculator.java

**

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock;
  8. /**
  9. * @author buhao
  10. * @version Calculator.java, v 0.1 2019-10-30 10:34 buhao
  11. */
  12. public class Calculator {
  13. /**
  14. * 加操作
  15. *
  16. * @param num1
  17. * @param num2
  18. * @return
  19. */
  20. public static int add(int num1, int num2) {
  21. return num1 + num2;
  22. }
  23. /**
  24. * 整型除操作
  25. *
  26. * @param num1
  27. * @param num2
  28. * @return
  29. */
  30. public static int divideInt(int num1, int num2) {
  31. return num1 / num2;
  32. }
  33. /**
  34. * 浮点型操作
  35. * @param num1
  36. * @param num2
  37. * @return
  38. */
  39. public static double divideDouble(double num1, double num2){
  40. return num1 / num2;
  41. }
  42. }

说明

这是一个很简单的计算器类。只写了三个方法,一个是加法的操作、一个整型的除法操作、一个浮点类型的除法操作。

3.3 开始单测 Calculator**.java**

3.3.1 创建单测类 CalculatorTest.groovy


  1. class CalculatorTest extends Specification {
  2. }

说明

这里一定要注意,之前我们已经说了 spock 是基于 groovy 。所以单测类的后缀不是 .java 而 .groovy。千万不要创建成普通 java 类了。否则创建没有问题,但是写一些 groovy 语法会报错。如果你用的是 IDEA 可以通过如下方式创建,以前创建 Java 类我们都是选择第一个选项,现在我们选择第三个 Groovy Class 就可以了。
image.png
另外就是 spock 的测试类需要继承 spock.lang.Specification 类。

3.3.2 验证加操作 - expect

  1. def "test add"(){
  2. expect:
  3. Calculator.add(1, 1) == 2
  4. }

说明

def 是 groovy 的关键字,可以用来定义变量跟方法名。后面 “test add” 是你单元测试的名称,也可以用中文。最后重点说明的是 expect 这个关键字。
expect 字面上的意思是期望,我们期望什么样的事情发生。在使用其它单测框架时,与之类似的是 assert 。比如 Assert.assertEquals(_Calculator.add(_1 + 1), 2) 这样,表示我们断言加操作传入1 与 1 相加结果为 2。如果结果是这样则用例通过,如果不是则用例失败。这与我们上面的代码功能上完成一致。
expect 的语法意义就是在 expect 的内,所有表达式成立则验证通过,反之有任一个不成立则验证失败。这里引入了一个的概念。怎么理解 spock 的块呢?我们上面说 spock 有良好的语义化及更好的阅读性就是因为这个块的作用。可以类比成 html 中的标签。html 的标签的范围是两个标签之间,而 spock 更简洁一点,从这个标签开始到下一个标签开始或代码结束的地方,就是他的范围。我们只要看到 expect 这个标签就明白,他的范围内都是我们预期要得到的结果。

3.3.3 验证加操作 - given - and

这里代码比较简单,参数我只使用了一次,所以直接写死。如果想复用,我就得把这些参数抽成变量。这个时候可以使用 spock 的 given 块。given 的语法意义相当于是一个初始化的代码块。

  1. def "test add with given"(){
  2. given:
  3. def num1 = 1
  4. def num2 = 1
  5. def result = 2
  6. expect:
  7. Calculator.add(num1, num2) == result
  8. }

当然你也可以像下面这样写,但是严重不推荐,因为虽然可以达到同样的效果,但是不符合 spock 的语义。就像我们一般是在 head 里面引入 js、css,但是你在 body 或者任何标签里都可以引入,语法没有问题但是破坏了语义,不便理解与维护。

  1. // 反倒
  2. def "test add with given"(){
  3. expect:
  4. def num1 = 1
  5. def num2 = 1
  6. def result = 2
  7. Calculator.add(num1, num2) == result
  8. }

如果你还想让语义更好一点,我们可以把参数与结果分开定义,这个时候可以使用 and 块。它的语法功能可以理解成同他上面最近的一个标签。

  1. def "test add with given and"(){
  2. given:
  3. def num1 = 1
  4. def num2 = 1
  5. and:
  6. def result = 2
  7. expect:
  8. Calculator.add(num1, num2) == result
  9. }

3.3.4 验证加操作 - expect - where

看了上面例子,可能觉得 spock 只是语义比较好,但是没有少写几行代码呀。别急,下面我们就来看 spock 的一大杀器 where

  1. def "test add with expect where"(){
  2. expect:
  3. Calculator.add(num1, num2) == result
  4. where:
  5. num1 | num2 | result
  6. 1 | 1 | 2
  7. 1 | 2 | 3
  8. 1 | 3 | 4
  9. }

where 块可以理解成准备测试数据的地方,他可以跟 expect 组合使用。上面代码里 expect 块里面定义了三个变量 num1、num2、result。这些数据我们可以在 where 块里定义。where 块使用了一种很像 markdown 中表格的定义方法。第一行或者说表头,列出了我们要传数据的变量名称,这里要与 expect 中对应,不能少但是可以多。其它行都是数据行,与表头一样都是通过 『 | 』 号分隔。通过这样,spock 就会跑 3 次用例,分别是 1 + 2 = 2、1 + 2 = 3、1 + 3 = 4 这些用例。怎么样?是不是很方便,后面再扩充用例只要再加一行数据就可以了。

3.3.5 验证加操作 - expect - where - @Unroll

上面这些用例都是正常可以跑通的,如果是 IDEA 跑完之后会如下所示:
image.png
那么现在我们看看如果有用例不通过会怎么样,把上面代码的最后一个 4 改成 5

  1. def "test add with expect where"(){
  2. expect:
  3. Calculator.add(num1, num2) == result
  4. where:
  5. num1 | num2 | result
  6. 1 | 1 | 2
  7. 1 | 2 | 3
  8. 1 | 3 | 5
  9. }

再跑一次,IDEA 会出现如下显示
image.png
左边标注出来的是用例执行结果,可以看出来虽然有 3 条数据,其中 2 条数据是成功,但是只会显示整体的成功与否,所以显示未通过。但是 3 条数据,我怎么知道哪条没通过呢?
右边标注出来的是 spock 打印的的错误日志。可以很清楚的看到,在 num1 为 1,num2 为 3,result 为 5 并且 他们之间的判断关系为 == 的结果是 false 才是正确的。 spock 的这个日志打印的是相当历害,如果是比较字符串,还会计算异常字符串与正确字符串之间的匹配度,有兴趣的同学,可以自行测试。
嗯,虽然可以通过日志知道哪个用例没通过,但是还是觉得有点麻烦。spock 也知道这一点。所以他还同时提供了一个 @Unroll 注解。我们在上面的代码上再加上这个注解:

  1. @Unroll
  2. def "test add with expect where unroll"(){
  3. expect:
  4. Calculator.add(num1, num2) == result
  5. where:
  6. num1 | num2 | result
  7. 1 | 1 | 2
  8. 1 | 2 | 3
  9. 1 | 3 | 5
  10. }

运行结果如下: image.png
通过添加 @Unroll 注解,spock 自动把上面的代码拆分成了 3 个独立的单测测试,分别运行,运行结果更清晰了。
那么还能更清晰吗?当然可以,我们发现 spock 拆分后,每个用例的名称其实都是你写的单测方法的名称,然后后面加一个数组下标,不是很直观。我们可以通过 groovy 的字符串语法,把变量放入用例名称中,代码如下:

  1. @Unroll
  2. def "test add with expect where unroll by #num1 + #num2 = #result"(){
  3. expect:
  4. Calculator.add(num1, num2) == result
  5. where:
  6. num1 | num2 | result
  7. 1 | 1 | 2
  8. 1 | 2 | 3
  9. 1 | 3 | 5
  10. }

如上,我们在方法名后加了一句 #num1 + #num2 = #result。这里有点类似我们在 mybatis 或者一些模板引擎中使用的方法。# 号拼接声明的变量就可以了,执行后结果如下。
image.png
这下更清晰了。
另外一点,就是 where 默认使用的是表格的这种形式:

  1. where:
  2. num1 | num2 | result
  3. 1 | 1 | 2
  4. 1 | 2 | 3
  5. 1 | 3 | 5

很直观,但是这种形式有一个弊端。上面 『 | 』 号对的这么整齐。都是我一个空格一个 TAG 按出来的。虽然语法不要求对齐,但是逼死强迫症。不过,好在还可以有另一种形式:

  1. @Unroll
  2. def "test add with expect where unroll arr by #num1 + #num2 = #result"(){
  3. expect:
  4. Calculator.add(num1, num2) == result
  5. where:
  6. num1 << [1, 1, 2]
  7. num2 << [1, 2, 3]
  8. result << [1, 3, 4]
  9. }

可以通过 『<<』 符(注意方向),把一个数组赋给变量,等同于上面的数据表格,没有表格直观,但是比较简洁也不用考虑对齐问题,这两种形式看个人喜好了。

3.3.6 验证整数除操作 - when - then

我们都知道一个整数除以0 会有抛出一个『/ by zero』异常,那么如果断言这个异常呢。用上面 expect 不太好操作,我们可以使用另一个类似的块 when … then

  1. @Unroll
  2. def "test int divide zero exception"(){
  3. when:
  4. Calculator.divideInt(1, 0)
  5. then:
  6. def ex = thrown(ArithmeticException)
  7. ex.message == "/ by zero"
  8. }

when … then 通常是成对出现的,它代表着当执行了 when 块中的操作,会出现 then 块中的期望。比如上面的代码说明了,当执行了 Calculator.divideInt(1, 0) 的操作,就一定会抛出 ArithmeticException 异常,并且异常信息是 / by zero

3.4 准备Spring测试类

上面我们已经学会了 spock 的基础用法,下面我们将学习与 spring 整合的知识,首先创建几个用于测试的demo 类

3.4.1 User.java

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock.model;
  8. import java.util.Objects;
  9. /**
  10. * @author buhao
  11. * @version User.java, v 0.1 2019-10-30 16:23 buhao
  12. */
  13. public class User {
  14. private String name;
  15. private Integer age;
  16. private String passwd;
  17. public User(String name, Integer age, String passwd) {
  18. this.name = name;
  19. this.age = age;
  20. this.passwd = passwd;
  21. }
  22. /**
  23. * Getter method for property <tt>passwd</tt>.
  24. *
  25. * @return property value of passwd
  26. */
  27. public String getPasswd() {
  28. return passwd;
  29. }
  30. /**
  31. * Setter method for property <tt>passwd</tt>.
  32. *
  33. * @param passwd value to be assigned to property passwd
  34. */
  35. public void setPasswd(String passwd) {
  36. this.passwd = passwd;
  37. }
  38. /**
  39. * Getter method for property <tt>name</tt>.
  40. *
  41. * @return property value of name
  42. */
  43. public String getName() {
  44. return name;
  45. }
  46. /**
  47. * Setter method for property <tt>name</tt>.
  48. *
  49. * @param name value to be assigned to property name
  50. */
  51. public void setName(String name) {
  52. this.name = name;
  53. }
  54. /**
  55. * Getter method for property <tt>age</tt>.
  56. *
  57. * @return property value of age
  58. */
  59. public Integer getAge() {
  60. return age;
  61. }
  62. /**
  63. * Setter method for property <tt>age</tt>.
  64. *
  65. * @param age value to be assigned to property age
  66. */
  67. public void setAge(Integer age) {
  68. this.age = age;
  69. }
  70. public User() {
  71. }
  72. @Override
  73. public boolean equals(Object o) {
  74. if (this == o) return true;
  75. if (o == null || getClass() != o.getClass()) return false;
  76. User user = (User) o;
  77. return Objects.equals(name, user.name) &&
  78. Objects.equals(age, user.age) &&
  79. Objects.equals(passwd, user.passwd);
  80. }
  81. @Override
  82. public int hashCode() {
  83. return Objects.hash(name, age, passwd);
  84. }
  85. }

3.4.2 UserDao.java

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock.dao;
  8. import cn.coder4j.study.example.spock.model.User;
  9. import org.springframework.stereotype.Component;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. /**
  13. * @author buhao
  14. * @version UserDao.java, v 0.1 2019-10-30 16:24 buhao
  15. */
  16. @Component
  17. public class UserDao {
  18. /**
  19. * 模拟数据库
  20. */
  21. private static Map<String, User> userMap = new HashMap<>();
  22. static {
  23. userMap.put("k",new User("k", 1, "123"));
  24. userMap.put("i",new User("i", 2, "456"));
  25. userMap.put("w",new User("w", 3, "789"));
  26. }
  27. /**
  28. * 通过用户名查询用户
  29. * @param name
  30. * @return
  31. */
  32. public User findByName(String name){
  33. return userMap.get(name);
  34. }
  35. }

3.4.3 UserService.java

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock.service;
  8. import cn.coder4j.study.example.spock.dao.UserDao;
  9. import cn.coder4j.study.example.spock.model.User;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.stereotype.Service;
  12. /**
  13. * @author buhao
  14. * @version UserService.java, v 0.1 2019-10-30 16:29 buhao
  15. */
  16. @Service
  17. public class UserService {
  18. @Autowired
  19. private UserDao userDao;
  20. public User findByName(String name){
  21. return userDao.findByName(name);
  22. }
  23. public void loginAfter(){
  24. System.out.println("登录成功");
  25. }
  26. public void login(String name, String passwd){
  27. User user = findByName(name);
  28. if (user == null){
  29. throw new RuntimeException(name + "不存在");
  30. }
  31. if (!user.getPasswd().equals(passwd)){
  32. throw new RuntimeException(name + "密码输入错误");
  33. }
  34. loginAfter();
  35. }
  36. }

3.4.3 Application.java

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock;
  8. import org.springframework.boot.SpringApplication;
  9. import org.springframework.boot.autoconfigure.SpringBootApplication;
  10. @SpringBootApplication
  11. public class Application {
  12. public static void main(String[] args) {
  13. SpringApplication.run(Application.class, args);
  14. }
  15. }

3.5 与 spring 集成测试

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock.service
  8. import cn.coder4j.study.example.spock.model.User
  9. import org.springframework.beans.factory.annotation.Autowired
  10. import org.springframework.boot.test.context.SpringBootTest
  11. import spock.lang.Specification
  12. import spock.lang.Unroll
  13. @SpringBootTest
  14. class UserServiceFunctionTest extends Specification {
  15. @Autowired
  16. UserService userService
  17. @Unroll
  18. def "test findByName with input #name return #result"() {
  19. expect:
  20. userService.findByName(name) == result
  21. where:
  22. name << ["k", "i", "kk"]
  23. result << [new User("k", 1, "123"), new User("i", 2, "456"), null]
  24. }
  25. @Unroll
  26. def "test login with input #name and #passwd throw #errMsg"() {
  27. when:
  28. userService.login(name, passwd)
  29. then:
  30. def e = thrown(Exception)
  31. e.message == errMsg
  32. where:
  33. name | passwd | errMsg
  34. "kd" | "1" | "${name}不存在"
  35. "k" | "1" | "${name}密码输入错误"
  36. }
  37. }

spock 与 spring 集成特别的简单,只要你加入了开头所说的 spock-springspring-boot-starter-test。再于测试代码的类上加上 @SpringBootTest 注解就可以了。想用的类直接注入进来就可以了,但是要注意的是这里只能算功能测试或集成测试,因为在跑用例时是会启动 spring 容器的,外部依赖也必须有。很耗时,而且有时候外部依赖本地也跑不了,所以我们通常都是通过 mock 来完成单元测试。

3.6 与 spring mock 测试

  1. /*
  2. * *
  3. * * blog.coder4j.cn
  4. * * Copyright (C) 2016-2019 All Rights Reserved.
  5. *
  6. */
  7. package cn.coder4j.study.example.spock.service
  8. import cn.coder4j.study.example.spock.dao.UserDao
  9. import cn.coder4j.study.example.spock.model.User
  10. import spock.lang.Specification
  11. import spock.lang.Unroll
  12. class UserServiceUnitTest extends Specification {
  13. UserService userService = new UserService()
  14. UserDao userDao = Mock(UserDao)
  15. def setup(){
  16. userService.userDao = userDao
  17. }
  18. def "test login with success"(){
  19. when:
  20. userService.login("k", "p")
  21. then:
  22. 1 * userDao.findByName("k") >> new User("k", 12,"p")
  23. }
  24. def "test login with error"(){
  25. given:
  26. def name = "k"
  27. def passwd = "p"
  28. when:
  29. userService.login(name, passwd)
  30. then:
  31. 1 * userDao.findByName(name) >> null
  32. then:
  33. def e = thrown(RuntimeException)
  34. e.message == "${name}不存在"
  35. }
  36. @Unroll
  37. def "test login with "(){
  38. when:
  39. userService.login(name, passwd)
  40. then:
  41. userDao.findByName("k") >> null
  42. userDao.findByName("k1") >> new User("k1", 12, "p")
  43. then:
  44. def e = thrown(RuntimeException)
  45. e.message == errMsg
  46. where:
  47. name | passwd | errMsg
  48. "k" | "k" | "${name}不存在"
  49. "k1" | "p1" | "${name}密码输入错误"
  50. }
  51. }

spock 使用 mock 也很简单,直接使用 Mock(类) 就可以了。如上代码 UserDao userDao = Mock(UserDao) 。上面写的例子中有几点要说明一下,以如下这个方法为例:

  1. def "test login with error"(){
  2. given:
  3. def name = "k"
  4. def passwd = "p"
  5. when:
  6. userService.login(name, passwd)
  7. then:
  8. 1 * userDao.findByName(name) >> null
  9. then:
  10. def e = thrown(RuntimeException)
  11. e.message == "${name}不存在"
  12. }

given、when、then 不用说了,大家已经很熟悉了,但是第一个 then 里面的 1 * userDao.findByName(name) >> null 是什么鬼?
首先,我们可以知道的是,一个用例中可以有多个 then 块,对于多个期望可以分别放在多个 then 中。
第二, 1 * xx 表示 期望 xx 操作执行了 1 次。1 * userDao.findByName(name) 就表现当执行 userService.login(name, passwd) 时我期望执行 1 次 userDao.findByName(name) 方法。如果期望不执行这个方法就是0 * xx,这在条件代码的验证中很有用,然后 >> null 又是什么意思?他代表当执行了 userDao.findByName(name) 方法后,我让他结果返回 null。因为 userDao 这个对象是我们 mock 出来的,他就是一个假对象,为了让后续流程按我们的想法进行,我可以通过『 >>』 让 spock 模拟返回指定数据。
第三,要注意第二个 then 代码块使用 ${name} 引用变量,跟标题的 #name 是不同的。

3.7 其它内容

3.7.1 公共方法

方法名 作用
setup() 每个方法执行前调用
cleanup() 每个方法执行后调用
setupSpec() 每个方法类加载前调用一次
cleanupSpec() 每个方法类执行完调用一次

这些方法通常用于测试开始前的一些初始化操作,和测试完成后的清理操作,如下:

  1. def setup() {
  2. println "方法开始前初始化"
  3. }
  4. def cleanup() {
  5. println "方法执行完清理"
  6. }
  7. def setupSpec() {
  8. println "类加载前开始前初始化"
  9. }
  10. def cleanupSpec() {
  11. println "所以方法执行完清理"
  12. }

3.7.2 @Timeout

对于某些方法,需要规定他的时间,如果运行时间超过了指定时间就算失败,这时可以使用 timeout 注解

  1. @Timeout(value = 900, unit = TimeUnit.MILLISECONDS)
  2. def "test timeout"(){
  3. expect:
  4. Thread.sleep(1000)
  5. 1 == 1
  6. }

注解有两个值,一个是 value 我们设置的数值,unit 是数值的单位。

3.7.3 with

  1. def "test findByName by verity"() {
  2. given:
  3. def userDao = Mock(UserDao)
  4. when:
  5. userDao.findByName("kk") >> new User("kk", 12, "33")
  6. then:
  7. def user = userDao.findByName("kk")
  8. with(user) {
  9. name == "kk"
  10. age == 12
  11. passwd == "33"
  12. }
  13. }

with 算是一个语法糖,没有他之前我们要判断对象的值只能,user.getXxx() == xx。如果属性过多也是挺麻烦的,用 with 包裹之后,只要在花括号内直接写属性名称即可,如上代码所示。

4. 其它

4.1 完整代码

因为篇幅有限,无法贴完所有代码,完整代码已上传 github

4.2 参考文档

本文在瞻仰了如下博主的精彩博文后,再加上自身的学习总结加工而来,如果本文在看的时候有不明白的地方可以看一下下方链接。

  1. Spock in Java 慢慢爱上写单元测试
  2. 使用Groovy+Spock轻松写出更简洁的单测
  3. Spock 测试框架的介绍和使用详解
  4. Spock 基于BDD测试
  5. Spock 官方文档
  6. Spock测试框架
  7. spock-testing-exceptions-with-data-tables

image.png