在静态测试中,介绍了使用Power Mock测试静态方法,返回的是一个指定的值,那能不能每次返回不同的值呢?本篇介绍Spock自带的Mock功能如何和Power Mock组合使用。
动态Mock静态方法(Spock Where + Power Mock)
看下什么场景需要这样做,代码如下:
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
|
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())) { System.out.println("source -> APP, currency -> CNY"); } else { System.out.println("source -> APP, currency -> !CNY"); } order.setType(1); } else if ("WAP".equals(HttpContextUtils.getCurrentSource())) { System.out.println("source -> WAP"); order.setType(2); } else if ("ONLINE".equals(HttpContextUtils.getCurrentSource())) { 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模拟的静态方法每次返回不同的值,代码如下:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package com.spock.example
import com.spock.example.service.OrderService import com.spock.example.utils.HttpContextUtils import com.spock.example.vo.UserVO import org.junit.runner.RunWith import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner import org.powermock.modules.junit4.PowerMockRunnerDelegate import org.spockframework.runtime.Sputnik import spock.lang.Specification import spock.lang.Unroll
@RunWith(PowerMockRunner.class) @PowerMockRunnerDelegate(Sputnik.class) @PrepareForTest([HttpContextUtils.class]) class OrderServiceStaticTest extends Specification {
def orderService = new OrderService()
void setup() { PowerMockito.mockStatic(HttpContextUtils.class) }
@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变量的用法,还是先看业务代码,了解这么做的背景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public List<OrderVO> convertUserOrders(List<OrderDTO> orders) { List<OrderVO> orderList = new ArrayList<>(); for (OrderDTO orderDTO : orders) { OrderVO orderVO = OrderMapper.INSTANCE.convert(orderDTO); 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是一个接口,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.spock.example.mapper;
import com.spock.example.dto.OrderDTO; import com.spock.example.vo.OrderVO; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers;
@Mapper public interface OrderMapper {
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如何写这个单元测试,代码如下:
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
|
@Unroll def "ConvertUserOrders"() { given: "mock掉OrderMapper的静态final变量INSTANCE,并结合spock设置动态返回值" def orderMapper = Mock(OrderMapper.class) Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper) orderMapper.convert(_ as OrderDTO) >> 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行代码:
1 2 3 4 5 6
| def orderMapper = Mock(OrderMapper.class) Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper) orderMapper.convert(_ as OrderDTO) >> order
|
这样就可以使用Spock mock结合power mock测试静态常量,达到覆盖if else不同分支逻辑的功能
执行结果

IDEA执行的遇到问题
IDEA2021报错:java: Internal error in the mapping processor: java.lang.NullPointerException解决

添加这段代码即可
-Djps.track.ap.dependencies=false