Spock代码讲解 - 静态方法测试
这一章主要讲解Spock如何扩展第三方power mock对静态方法进行测试
实现原理
前面的文章讲到Spock的单测代码是继承自Specification基类,而Specification又是基于Junit的注解@RunWith()实现的:
@RunWith(Sputnik.class)
@SuppressWarnings("UnusedDeclaration")
public abstract class Specification extends MockingApi {
powermock的PowerMockRunner也是继承自Junit,所以使用powermock的@PowerMockRunnerDelegate()注解可以指定Spock的父类Sputnik去代理运行power mock,这样就可以在Spock里使用powermock去模拟静态方法、final方法、私有方法等。
其实Spock自带的GroovyMock可以对groovy文件的静态方法mock,但对Java代码的支持不完整,只能mock当前Java类的静态方法,官方给出的解释如下:
(http://spockframework.org/spock/docs/1.3/all_in_one.html#_mocking_static_methods)
因为我们项目中存在很多调用静态方法的代码,现阶段考虑重构业务代码的成本过高,所以这里使用扩展power mock的方式测试静态方法。
Spock 代理 power mock
先看下需要测试的业务代码示例:
public UserVO getUserByIdStatic(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());
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));
}
// 静态方法调用 身份证工具类
Map<String, String> idMap = IDNumberUtils.getBirAgeSex(userDTO.getIdNo());
userVO.setAge(idMap.get("age")!=null ? Integer.parseInt(idMap.get("age")) : 0);
// 静态方法调用 记录日志
LogUtils.info("response user:", userVO.toString());
return userVO;
}
在倒数第4行和倒数第2行代码分别调用了 “身份证工具类IDNumberUtils.getBirAgeSex()” 和 “LogUtils.info()” 日志记录的方法,如果要对这两个静态方法进行mock,我们可以使用Spock+power mock的方式:
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.spockframework.runtime.Sputnik
import spock.lang.Specification
/**
* 测试静态方法mock
* @Author: www.javakk.com
* @Description: 公众号:Java老K
* @Date: Created in 20:53 2020/7/16
* @Modified By:
*/
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([LogUtils.class, IDNumberUtils.class])
@SuppressStaticInitializationFor(["com.javakk.spock.util.LogUtils"])
class UserServiceStaticTest extends Specification {
def processor = new UserService()
def dao = Mock(UserDao)
void setup() {
processor.userDao = dao
// mock静态类
PowerMockito.mockStatic(LogUtils.class)
PowerMockito.mockStatic(IDNumberUtils.class)
}
def "GetUserByIdStatic"() {
given: "设置请求参数"
def user1 = new UserDTO(id:1, name:"张三", province: "上海")
def user2 = new UserDTO(id:2, name:"李四", province: "江苏")
def idMap = ["birthday": "1992-09-18", "sex": "男", "age": "28"]
and: "mock掉接口返回的用户信息"
dao.getUserInfo() >> [user1, user2]
and: "mock静态方法返回值"
PowerMockito.when(IDNumberUtils.getBirAgeSex(Mockito.any())).thenReturn(idMap)
when: "调用获取用户信息方法"
def response = processor.getUserByIdStatic(1)
then: "验证返回结果是否符合预期值"
with(response) {
name == "张三"
abbreviation == "沪"
postCode == 200000
age == 28
}
}
}
在UserServiceStaticTest类的头部使用@PowerMockRunnerDelegate(Sputnik.class)注解,交给Spock代理执行,这样既可以使用 Spock + groovy 的各种功能,又可以使用power mock的对静态,final等方法的mock
@SuppressStaticInitializationFor([“com.javakk.spock.util.LogUtils”])
这行代码的作用是限制LogUtils类里的静态代码块初始化,因为LogUtils类在第一次调用时会加载一些本地资源配置,比较耗费时间,所以可以使用power mock禁止初始化。
然后在setup()方法里对两个静态类进行mock设置,PowerMockito.mockStatic(LogUtils.class),PowerMockito.mockStatic(IDNumberUtils.class)
最后在GetUserByIdStatic测试方法里对getBirAgeSex()方法指定返回默认值:PowerMockito.when(IDNumberUtils.getBirAgeSex(Mockito.any())).thenReturn(idMap)
(power mock的具体用法网上资料很多,这里不展开说明)
运行时在控制台会输出:
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
这是powermock的警告信息,不影响运行结果
(关注公众号: java老k 回复spock获取全部源码)
另外,如果你的单元测试代码不需要对静态方法, final方法mock, 就没必要使用power mock, 使用Spock自带的Mock()就足够了
因为power mock的原理是在编译期通过ASM字节码修改工具修改我们的代码,然后使用自己的classLoader加载,加载的静态方法越多会相应的增加测试时长。
Spock高级用法 - 动态mock
这一章讲解Spock自带的mock功能如何和power mock组合使用,发挥更强大的作用
动态mock静态方法 (spock where + power mock)
在上一篇的例子中使用power mock让静态方法返回一个指定的值,那能不能每次返回不同的值呢?
我们先看下什么场景需要这样做:
/**
* 静态方法多分支场景
* @param userVO
* @return
*/
public List<OrderVO> getUserOrdersBySource(UserVO userVO){
List<OrderVO> orderList = new ArrayList<>();
OrderVO order = new OrderVO();
if ("APP".equals(HttpContextUtils.getCurrentSource())) { // 手机来源
if("CNY".equals(HttpContextUtils.getCurrentCurrency())){ // 人民币
// TODO 针对App端的订单,并且请求币种为人民币的业务逻辑...
System.out.println("source -> APP, currency -> CNY");
} else {
System.out.println("source -> APP, currency -> !CNY");
}
order.setType(1);
} else if ("WAP".equals(HttpContextUtils.getCurrentSource())) { // H5来源
// TODO 针对H5端的业务逻辑...
System.out.println("source -> WAP");
order.setType(2);
} else if ("ONLINE".equals(HttpContextUtils.getCurrentSource())) { // PC来源
// TODO 针对PC端的业务逻辑...
System.out.println("source -> ONLINE");
order.setType(3);
}
orderList.add(order);
return orderList;
}
这段代码的 if else 分支逻辑主要是依据HttpContextUtils这个工具类的静态方法 getCurrentSource() 和 getCurrentCurrency() 的返回值决定流程的。
这样的业务代码也是我们平时写单测经常遇到的场景,如果能让HttpContextUtils.getCurrentSource() 静态方法每次mock出不同的值,就可以很方便的覆盖if else的全部分支逻辑。
Spock的where标签可以方便的和power mock结合使用,让power mock模拟的静态方法每次返回不同的值,代码如下:
/**
* 测试静态方法mock
* @Author: www.javakk.com
* @Description: 公众号:Java老K
*/
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([HttpContextUtils.class])
class OrderServiceStaticTest extends Specification {
def orderService = new OrderService()
void setup() {
// mock静态类
PowerMockito.mockStatic(HttpContextUtils.class)
}
/**
* 测试spock的mock和power mock静态方法组合用法的场景
*/
@Unroll
def "当来源是#source时,订单类型为:#type"() {
given: "mock当前上下文的请求来源"
PowerMockito.when(HttpContextUtils.getCurrentSource()).thenReturn(source)
and: "mock当前上下文的币种"
PowerMockito.when(HttpContextUtils.getCurrentCurrency()).thenReturn(currency)
when: "调用获取用户订单列表"
def orderList = orderService.getUserOrdersBySource(new UserVO())
then: "验证返回结果是否符合预期值"
with(orderList) {
it[0].type == type
}
where: "表格方式验证订单信息的分支场景"
source | currency || type
"APP" | "CNY" || 1
"APP" | "USD" || 1
"WAP" | "" || 2
"ONLINE" | "" || 3
}
}
powermock的thenReturn方法返回的值是 source 和 currency 两个变量,不是具体的数据,这两个变量对应where标签里的前两列 “source | currency”
这样的写法就可以每次测试业务方法时,让HttpContextUtils.getCurrentSource()和HttpContextUtils.getCurrentCurrency() 返回不同的来源和币种,就能轻松的覆盖if和else的分支代码。
即Spock使用where表格的方式让power mock具有了动态mock的功能
动态mock接口 (spock mock + power mock + where)
上个例子讲了把power mock返回的mock值作为变量放在where里使用,以达到动态mock静态方法的功能,这里再介绍一种动态mock 静态+final变量的用法,还是先看业务代码,了解这么做的背景:
/**
* 静态final变量场景
* @param orders
* @return
*/
public List<OrderVO> convertUserOrders(List<OrderDTO> orders){
List<OrderVO> orderList = new ArrayList<>();
for (OrderDTO orderDTO : orders) {
OrderVO orderVO = OrderMapper.INSTANCE.convert(orderDTO); // VO DTO 属性转换
if (1 == orderVO.getType()) {
orderVO.setOrderDesc("App端订单");
} else if(2 == orderVO.getType()) {
orderVO.setOrderDesc("H5端订单");
} else if(3 == orderVO.getType()) {
orderVO.setOrderDesc("PC端订单");
}
orderList.add(orderVO);
}
return orderList;
}
这段代码里的for循环第一行调用了"OrderMapper.INSTANCE.convert()"转换方法,将orderDTO转换为orderVO,然后根据type的值走不同的分支。
而OrderMapper是一个接口,代码如下:
import org.mapstruct.Mapper;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
/**
* 订单属性转换
*/
@Mapper
public interface OrderMapper {
// 即使不用static final修饰,接口里的变量默认也是静态、final的
static final OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mappings({})
OrderVO convert(OrderDTO requestDTO);
}
"INSTANCE"是接口OrderMapper里定义的变量,接口里的变量默认都是static final的,所以我们要先把这个INSTANCE静态final变量mock掉,这样才能调用它的方法convert() 返回我们想要的值。
OrderMapper这个接口是mapstruct工具的用法,mapstruct是做对象属性映射的一个工具,它会自动生成OrderMapper接口的实现类,生成对应的set、get方法,把orderDTO的属性值赋给orderVO属性,比使用反射的方式好很多(具体用法自行百度)
看下Spock如何写这个单元测试:
/**
* 测试spock的mock和powermock静态final变量结合的用法
*/
@Unroll
def "ConvertUserOrders"() {
given: "mock掉OrderMapper的静态final变量INSTANCE,并结合spock设置动态返回值"
def orderMapper = Mock(OrderMapper.class)
Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper)
orderMapper.convert(_) >> order
when: "调用用户订单转换方法"
def userOrders = orderService.convertUserOrders([new OrderDTO()])
then: "验证返回结果是否符合预期值"
with(userOrders) {
it[0].orderDesc == desc
}
where: "表格方式验证订单属性转换结果"
order || desc
new OrderVO(type: 1) || "App端订单"
new OrderVO(type: 2) || "H5端订单"
new OrderVO(type: 3) || "PC端订单"
}
-
首先使用Spock自带的Mock()方法,将OrderMapper类mock为一个模拟对象orderMapper,“def orderMapper = Mock(OrderMapper.class)”
-
然后使用power mock的Whitebox.setInternalState()对OrderMapper接口的static final常量INSTANCE赋值(Spock不支持静态常量的mock),赋的值正是使用spock mock的对象orderMapper
-
使用Spock的mock模拟convert()方法调用,“orderMapper.convert(_) >> order”,再结合where表格,实现动态mock接口的功能
主要就是这3行代码:
def orderMapper = Mock(OrderMapper.class) // 先使用Spock的mock
Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper) // 将第一步mock的对象orderMapper 使用power mock赋值给静态常量INSTANCEmock
orderMapper.convert(_) >> order // 结合where模拟不同的返回值
这样就可以使用Spock mock结合power mock测试静态常量,达到覆盖if else不同分支逻辑的功能。
由此可见Spock可以和power mock深度结合,测试一些特殊的场景,也可以按照这个思路继续挖掘其他用法。
(关注公众号: java老k 回复spock获取全部源码)
文章来源:http://javakk.com/category/spock
互联网一线java开发老兵,工作10年有余,梦想敲一辈子代码,以梦为码,不负韶华!
扫码关注Java老K,获取更多Java技术干货。