你所熟知的单元测试工具有什么呢? JUnit? JMock? 还是 Mockito? 相较于以上的传统单元测试框架,Spock 是一个更加优雅的测试框架,在开发效率、可读性和维护性方面都有很大的提升。
Spock 结合了 Groovy动态语言的特点,提供了各种各样的标签,如 given
, when
, then
, expect
, where
等,并且采用简单、通用的结构化的描述语言,使得测试用例更加易读、高效。
对于分布式微服务架构,服务与服务之间的依赖关系错综复杂。即使是在同一个服务内也会耦合多个模块,业务功能的结果需要依赖于其他模块的数据。所以,如果我们想要测试自己的代码时,就需要将其他模块进行mock,这样才可以验证我们的代码是否正确是否符合逻辑结果的预期。
由于JUnit单纯用于测试,并不提供Mock功能,以及JMock、Mockito等框架的Mock功能相对繁琐。Spock通过提供规范性的描述,定义多种标签等,从语义层面规范了代码的编写。
让我们来试试Spock吧!
导入Spock依赖
1 2 3 4 5
| <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>2.1-groovy-3.0</version> </dependency>
|
导入可以运行Groovy代码的插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <build> <plugins> <plugin> <groupId>org.codehaus.gmavenplus</groupId> <artifactId>gmavenplus-plugin</artifactId> <version>1.13.1</version> <executions> <execution> <goals> <goal>compile</goal> <goal>compileTests</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
|
对于下面一个需要测试的模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class SpockTest { public static double calc(double income) { BigDecimal tax; BigDecimal salary = BigDecimal.valueOf(income); if (income <= 0) { return 0; } if (income > 0 && income <= 3000) { BigDecimal taxLevel = BigDecimal.valueOf(0.03); tax = salary.multiply(taxLevel); } else if (income > 3000 && income <= 12000) { BigDecimal taxLevel = BigDecimal.valueOf(0.1); BigDecimal base = BigDecimal.valueOf(210); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 12000 && income <= 25000) { BigDecimal taxLevel = BigDecimal.valueOf(0.2); BigDecimal base = BigDecimal.valueOf(1410); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 25000 && income <= 35000) { BigDecimal taxLevel = BigDecimal.valueOf(0.25); BigDecimal base = BigDecimal.valueOf(2660); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 35000 && income <= 55000) { BigDecimal taxLevel = BigDecimal.valueOf(0.3); BigDecimal base = BigDecimal.valueOf(4410); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 55000 && income <= 80000) { BigDecimal taxLevel = BigDecimal.valueOf(0.35); BigDecimal base = BigDecimal.valueOf(7160); tax = salary.multiply(taxLevel).subtract(base); } else { BigDecimal taxLevel = BigDecimal.valueOf(0.45); BigDecimal base = BigDecimal.valueOf(15160); tax = salary.multiply(taxLevel).subtract(base); } return tax.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); } }
|
我们可以使用Spock来进行测试,编写一个groovy程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Subject(SpockTest) class TestSpock extends Specification { @Unroll def "个税计算,收入:#income, 个税:#result"() { expect: "when + then 的组合" SpockTest.calc(income) == result
where: "表格方式测试不同的分支逻辑" income || result -1 || 0 0 || 0 2999 || 89.97 3000 || 90.0 3001 || 90.1 11999 || 989.9 12000 || 990.0 12001 || 990.2 24999 || 3589.8 25000 || 3590.0 25001 || 3590.25 34999 || 6089.75 35000 || 6090.0 35001 || 6090.3 54999 || 12089.7 55000 || 12090 55001 || 12090.35 79999 || 20839.65 80000 || 20840.0 80001 || 20840.45 } }
|
使用Spock提供的where标签,可以让我们通过表格的方式来测试多种分支。表格方式测试覆盖分支场景更加直观,开发效率高,更适合敏捷开发。
除了where标签,Spock还提供了很多其他标签,如 given
, when
, then
, expect
等,每一种标签对应一种语义,让单元测试代码结构具有层次感,功能模块划分更加清晰,也便于后期的维护。
来看这样一个待测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public StudentVO getStudentById(int id) { List<StudentDTO> students = studentDao.getStudentInfo(); StudentDTO studentDTO = students.stream().filter(u -> u.getId() == id).findFirst().orElse(null); StudentVO studentVO = new StudentVO(); if (studentDTO == null) { return studentVO; } studentVO.setId(studentDTO.getId()); studentVO.setName(studentDTO.getName()); studentVO.setSex(studentDTO.getSex()); studentVO.setAge(studentDTO.getAge()); if ("上海".equals(studentDTO.getProvince())) { studentVO.setAbbreviation("沪"); studentVO.setPostCode("200000"); } if ("北京".equals(studentDTO.getProvince())) { studentVO.setAbbreviation("京"); studentVO.setPostCode("100000"); } return studentVO; }
|
我们可以直接使用Spock来进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class StudentSerivceSpec extends Specification { def dao = Mock(StudentDao) def studentService = new StudentService(dao)
def "测试 getStudentById"() { given: "设置请求参数" def student1 = new StudentDTO(1, "张三", "男", 18, "上海") def student2 = new StudentDTO(2, "李四", "女", 20, "北京")
and: "mock studentDao的返回值" dao.getStudentInfo() >> [student1, student2]
when: "获取学生信息" def response = studentService.getStudentById(1)
then: "验证返回值" with(response) { id == 1 abbreviation == "沪" postCode == "200000" } } }
|
Spock会强制要求编程人员使用given、when、then这样的语义标签(至少一个),否则编译不通过,这样就能保证代码更加规范,结构模块化,边界范围清晰,可读性强,便于扩展和维护。而且使用了自然语言描述测试步骤,让非技术人员也能看懂测试代码(given表示输入条件,when触发动作,then验证输出结果)。
dao.getStudentInfo() >> [student1, student2]
表示当调用 dao.getStudentInfo()
方法时,返回 student1
和 student2
两个对象,以列表的形式。即两个右箭头>>表示模拟getStudentInfo接口的返回结果,[]中括号表示返回的是List类型。
接下来看看循环测试
1 2 3 4 5 6 7 8 9 10 11
| public void calculatePrice(OrderVO order){ BigDecimal amount = BigDecimal.ZERO; for (SkuVO sku : order.getSkus()) { Integer skuId = sku.getSkuId(); BigDecimal skuPrice = sku.getSkuPrice(); BigDecimal discount = BigDecimal.valueOf(discountDao.getDiscount(skuId)); BigDecimal price = skuPrice * discount; amount = amount.add(price); } order.setAmount(amount.setScale(2, BigDecimal.ROUND_HALF_DOWN)); }
|
使用Spock的循环测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def "测试 calculatePrice"() { given: "设置请求参数" def sku1 = new SkuVO(1, BigDecimal.valueOf(100)) def sku2 = new SkuVO(2, BigDecimal.valueOf(200)) def order = new OrderVO() order.setSkus([sku1, sku2])
and: "mock discountDao的返回值" def discountDao = Mock(DiscountDao) 2 * discountDao.getDiscount(1) >> 0.9 >> 0.8
when: "计算价格" service.calculatePrice(order)
then: "验证返回值" order.amount == 260 }
|
2 * discountDao.getDiscount(_) >> 0.95 >> 0.8
在for循环中一共调用了2次,第一次返回结果0.9,第二次返回结果0.8,最后再进行验证。
接下来考虑使用Spring自动注入的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Service public class StudentService { @Autowired private StudentDao studentDao; public StudentVO getStudentById(int id) { List<StudentDTO> students = studentDao.getStudentInfo(); StudentDTO studentDTO = students.stream().filter(u -> u.getId() == id).findFirst().orElse(null); StudentVO studentVO = new StudentVO(); if (studentDTO == null) { return studentVO; } studentVO.setId(studentDTO.getId()); studentVO.setName(studentDTO.getName()); studentVO.setSex(studentDTO.getSex()); studentVO.setAge(studentDTO.getAge()); if ("上海".equals(studentDTO.getProvince())) { studentVO.setAbbreviation("沪"); studentVO.setPostCode("200000"); } if ("北京".equals(studentDTO.getProvince())) { studentVO.setAbbreviation("京"); studentVO.setPostCode("100000"); } return studentVO; } }
|
其中studentDao是使用Spring注入的实例对象,加上我们并不知道studentDao的具体实现,我们依旧可以使用Spock进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class StudentServiceSpec extends Specification { def studentDao = Mock(StudentDao) def tester = new StudentService(studentDao: studentDao)
def "test getStudentById"() { given: "设置请求参数" def student1 = new StudentDTO(id: 1, name: "张三", province: "北京") def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")
and: "mock studentDao返回值" studentDao.getStudentInfo() >> [student1, student2]
when: "获取学生信息" def response = tester.getStudentById(1)
then: "结果验证" with(response) { id == 1 abbreviation == "京" postCode == "100000" } } }
|
def studentDao = Mock(StudentDao)
这一行代码使用Spock自带的Mock方法,构造一个studentDao的Mock对象。
在given块中,我们使用了new StudentDTO(id: 1, name: "张三", province: "北京")
来构造一个StudentDTO对象,这是Groovy帮助我们实现的,Groovy默认会提供一个包含所有对象属性的构造方法。而且调用方式上可以指定属性名,类似于key:value的语法,非常人性化,方便在属性多的情况下构造对象,这样即使我们不知道StudentDTO有哪些构造函数,也能够构造出对象。
对于要指定返回多个值的话,可以使用3个右箭头>>>,比如:studentDao.getStudentInfo() >>> [[student1,student2],[student3,student4],[student5,student6]]
。
也可以写成这样:studentDao.getStudentInfo() >> [student1,student2] >> [student3,student4] >> [student5,student6]
。
对于函数getStudentInfo(String id)
方法,有个参数id,这种情况下如果使用Spock的Mock模拟调用的话,可以使用下划线_匹配参数,表示任何类型的参数,多个逗号隔开
,如果出现了函数重载,即存在多个同名函数,可以使用_ as参数类型
的方式区别调用。