在上一篇讲单元测试代码可读性和维护性的问题时举了一种业务场景,即接口调用,咱们的用户服务须要调用用户中心接口获取用户信息,代码以下:html
/** * 用户服务 * @author 公众号:Java老K * 我的博客:www.javakk.com */ @Service public class UserService { @Autowired UserDao userDao; @Autowired MoneyDAO moneyDAO; public UserVO getUserById(int uid){ List<UserDTO> users = userDao.getUserInfo(); UserDTO userDTO = users.stream().filter(u -> u.getId() == uid).findFirst().orElse(null); UserVO userVO = new UserVO(); if(null == userDTO){ return userVO; } userVO.setId(userDTO.getId()); userVO.setName(userDTO.getName()); userVO.setSex(userDTO.getSex()); userVO.setAge(userDTO.getAge()); // 显示邮编 if("上海".equals(userDTO.getProvince())){ userVO.setAbbreviation("沪"); userVO.setPostCode(200000); } if("北京".equals(userDTO.getProvince())){ userVO.setAbbreviation("京"); userVO.setPostCode(100000); } // 手机号处理 if(null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())){ userVO.setTelephone(userDTO.getTelephone().substring(0,3)+"****"+userDTO.getTelephone().substring(7)); } return userVO; } }
其中userDao
是使用spring注入的用户中心服务的实例对象,咱们只有拿到了用户中心的返回的users
,才能继续下面的逻辑(根据uid筛选用户,DTO和VO转换,邮编、手机号处理等)java
因此正常的作法是把userDao的getUserInfo()
方法mock掉,模拟一个咱们指定的值,由于咱们真正关心的是拿到users后本身代码的逻辑,这是咱们须要重点验证的地方spring
按照上面的思路使用Spock编写的测试代码以下:数组
package com.javakk.spock.service import com.javakk.spock.dao.UserDao import spock.lang.Specification import spock.lang.Unroll /** * 用户服务测试类 * @author 公众号:Java老K * 我的博客:www.javakk.com */ class UserServiceTest extends Specification { def userService = new UserService() def userDao = Mock(UserDao) void setup() { userService.userDao = userDao } def "GetUserById"() { given: "设置请求参数" def user1 = new UserDTO(id:1, name:"张三", province: "上海") def user2 = new UserDTO(id:2, name:"李四", province: "江苏") and: "mock掉接口返回的用户信息" userDao.getUserInfo() >> [user1, user2] when: "调用获取用户信息方法" def response = userService.getUserById(1) then: "验证返回结果是否符合预期值" with(response) { name == "张三" abbreviation == "沪" postCode == 200000 } } }
若是要看junit如何实现能够参考上一篇的对比图,这里主要讲解spock的代码:(从上往下)ide
def userDao = Mock(UserDao)
这一行代码使用spock自带的Mock方法构造一个userDao的mock对象,若是要模拟userDao方法的返回,只需userDao.方法名() >> 模拟值
的方式,两个右箭头的方式便可函数
setup
方法是每一个测试用例运行前的初始方法,相似于junit的@before
post
GetUserById
方法是单测的主要方法,能够看到分为4个模块:given
、and
、when
、then
,用来区分不一样单测代码的做用:单元测试
每一个标签后面的双引号里能够添加描述,说明这块代码的做用(非强制),如"when: "调用获取用户信息方法""测试
由于spock使用groovy做为单测开发语言,因此代码量上比使用java写的会少不少,好比given模块里经过构造函数的方式建立请求对象ui
given: "设置请求参数" def user1 = new UserDTO(id:1, name:"张三", province: "上海") def user2 = new UserDTO(id:2, name:"李四", province: "江苏")
实际上UserDTO.java
这个类并无3个参数的构造函数,是groovy帮咱们实现的,groovy默认会提供一个包含全部对象属性的构造函数
并且调用方式上能够指定属性名,相似于key:value的语法,很是人性化,方便咱们在属性多的状况下构造对象,若是使用java写,可能就要调用不少setXXX()
方法才能完成对象初始化的工做
and: "mock掉接口返回的用户信息" userDao.getUserInfo() >> [user1, user2]
这个就是spock的mock用法,即当调用userDao.getUserInfo()
方法时返回一个List,list的建立也很简单,中括号"[]"即表示list,groovy会根据方法的返回类型自动匹配是数组仍是list,而list里的对象就是以前given块里构造的user对象
其中 ">>" 就是指定返回结果,相似mockito的when().thenReturn()
语法,但更简洁一些
若是要指定返回多个值的话可使用3个右箭头">>>",好比:
userDao.getUserInfo() >>> [[user1,user2],[user3,user4],[user5,user6]]
也能够写成这样:
userDao.getUserInfo() >> [user1,user2] >> [user3,user4] >> [user5,user6]
即每次调用userDao.getUserInfo()
方法返回不一样的值
若是mock的方法带有入参的话,好比下面的业务代码:
public List<UserDTO> getUserInfo(String uid){ // 模拟用户中心服务接口调用 List<UserDTO> users = new ArrayList<>(); return users; }
这个getUserInfo(String uid)
方法,有个参数uid,这种状况下若是使用spock的mock模拟调用的话,可使用下划线"_"匹配参数,表示任何类型的参数,多个逗号隔开,相似与mockito的any()
方法
若是类中存在多个同名函数,能够经过 "_ as 参数类型" 的方式区别调用,相似下面的语法:
// _ 表示匹配任意类型参数 List<UserDTO> users = userDao.getUserInfo(_); // 若是有同名的方法,使用as指定参数类型区分 List<UserDTO> users = userDao.getUserInfo(_ as String);
when模块里是真正调用要测试方法的入口:userService.getUserById()
then模块做用是验证被测方法的结果是否正确,符合预期值,因此这个模块里的语句必须是boolean表达式,相似于junit的assert断言机制,但你没必要显示的写assert,这也是一种约定优于配置的思想
then
块中使用了spock的with
功能,能够验证返回结果response对象内部的多个属性是否符合预期值,这个相对于junit的assertNotNull
或assertEquals
的方式更简单一些
上面的业务代码有3个if判断,分别是对邮编和手机号的处理逻辑:
// 显示邮编 if("上海".equals(userDTO.getProvince())){ userVO.setAbbreviation("沪"); userVO.setPostCode(200000); } if("北京".equals(userDTO.getProvince())){ userVO.setAbbreviation("京"); userVO.setPostCode(100000); } // 手机号处理 if(null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())){ userVO.setTelephone(userDTO.getTelephone().substring(0,3)+"****"+userDTO.getTelephone().substring(7)); }
如今的单元测试若是要彻底覆盖这3个分支就须要构造不一样的请求参数屡次调用被测试方法才能走到不一样的分支,在上一篇中介绍了spock的where
标签能够很方便的实现这种功能,代码以下:
@Unroll def "当输入的用户id为:#uid 时返回的邮编是:#postCodeResult,处理后的电话号码是:#telephoneResult"() { given: "mock掉接口返回的用户信息" userDao.getUserInfo() >> users when: "调用获取用户信息方法" def response = userService.getUserById(uid) then: "验证返回结果是否符合预期值" with(response) { postCode == postCodeResult telephone == telephoneResult } where: "表格方式验证用户信息的分支场景" uid | users || postCodeResult | telephoneResult 1 | getUser("上海", "13866667777") || 200000 | "138****7777" 1 | getUser("北京", "13811112222") || 100000 | "138****2222" 2 | getUser("南京", "13833334444") || 0 | null } def getUser(String province, String telephone){ return [new UserDTO(id: 1, name: "张三", province: province, telephone: telephone)] }
where
模块第一行代码是表格的列名,多个列使用"|"单竖线隔开,"||"双竖线区分输入和输出变量,即左边是输入值,右边是输出值
格式以下:
输入参数1 | 输入参数2 || 输出结果1 | 输出结果2
并且intellij idea支持format格式化快捷键,由于表格列的长度不同,手动对齐比较麻烦
表格的每一行表明一个测试用例,即被测方法被测试了3次,每次的输入和输出都不同,恰好能够覆盖所有分支状况
好比uid、users都是输入条件,其中users对象的构造调用了getUser
方法,每次测试业务代码传入不一样的user值,postCodeResult
、telephoneResult
表示对返回的response对象的属性判断是否正确
第一行数据的做用是验证返回的邮编是不是"200000",第二行是验证邮编是不是"100000",第三行的邮编是不是"0"(由于代码里没有对南京的邮编进行处理,因此默认值是0)
这个就是where
+with
的用法,更符合咱们实际测试的场景,既能覆盖多种分支,又能够对复杂对象的属性进行验证
其中在第2行定义的测试方法名是使用了groovy的字面值特性:
@Unroll def "当输入的用户id为:#uid 时返回的邮编是:#postCodeResult,处理后的电话号码是:#telephoneResult"() {
即把请求参数值和返回结果值的字符串里动态替换掉,"#uid、#postCodeResult、#telephoneResult" 井号后面的变量是在方法内部定义的,前面加上#号,实现占位符的功能
@Unroll
注解,能够把每一次调用做为一个单独的测试用例运行,这样运行后的单测结果更直观:
并且其中一行测试结果不对,spock的错误提示信息也很详细,方便排查(好比咱们把第2条测试用例返回的邮编改为"100001"):
能够看出第2条测试用例失败,错误信息是postCodeResult
的预期结果和实际结果不符,业务代码逻辑返回的邮编是"100000",而咱们预期的邮编是"100001",这样你就能够排查是业务代码逻辑有问题仍是咱们的断言不对。
经过这个例子你们能够看到Spock结合groovy语言在测试多个分支场景时的优点。
(完整的源码在公众号【java老k】里回复spock获取)