在静态测试中,介绍了使用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
/**
* 静态方法多分支场景
*
* @param userVO 用户对象
* @return 返回订单对象List
*/
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);
// H5来源
} else if ("WAP".equals(HttpContextUtils.getCurrentSource())) {
// TODO 针对H5端的业务逻辑...
System.out.println("source -> WAP");
order.setType(2);
// PC来源
} else if ("ONLINE".equals(HttpContextUtils.getCurrentSource())) {
// 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模拟的静态方法每次返回不同的值,代码如下:

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

/**
* @author jinglv* @date 2021/7/29 11:44 上午
*/
@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静态方法组合用法的场景
* @return
*/
@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的功能

执行结果

image-20210729115032555

动态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
/**
* 静态final变量场景
*
* @param orders 订单信息
* @return 订单转换后的信息
*/
public List<OrderVO> convertUserOrders(List<OrderDTO> orders) {
List<OrderVO> orderList = new ArrayList<>();
for (OrderDTO orderDTO : orders) {
// VO DTO 属性转换
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;

/**
* 订单属性转换
*
* @author jinglv
* @date 2021/7/29 11:52 上午
*/
@Mapper
public interface OrderMapper {

/**
* 即使不用static final修饰,接口里的变量默认也是静态、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如何写这个单元测试,代码如下:

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
/**
* 测试spock的mock和powermock静态final变量结合的用法
*/
@Unroll
def "ConvertUserOrders"() {
given: "mock掉OrderMapper的静态final变量INSTANCE,并结合spock设置动态返回值"
// 先使用Spock的mock
def orderMapper = Mock(OrderMapper.class)
// 将第一步mock的对象orderMapper 使用power mock赋值给静态常量INSTANCE mock
Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper)
// 结合where模拟不同的返回值
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端订单"
}
  1. 首先使用Spock自带的Mock()方法,将OrderMapper类mock为一个模拟对象orderMapper,”def orderMapper = Mock(OrderMapper.class)”

  2. 然后使用power mock的Whitebox.setInternalState()对OrderMapper接口的static final常量INSTANCE赋值(Spock不支持静态常量的mock),赋的值正是使用spock mock的对象orderMapper

  3. 使用Spock的mock模拟convert()方法调用,”orderMapper.convert(_) >> order”,再结合where表格,实现动态mock接口的功能

主要就是这3行代码:

1
2
3
4
5
6
// 先使用Spock的mock
def orderMapper = Mock(OrderMapper.class)
// 将第一步mock的对象orderMapper 使用power mock赋值给静态常量INSTANCE mock
Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper)
// 结合where模拟不同的返回值
orderMapper.convert(_ as OrderDTO) >> order

这样就可以使用Spock mock结合power mock测试静态常量,达到覆盖if else不同分支逻辑的功能

执行结果

image-20210729121000849

IDEA执行的遇到问题

IDEA2021报错:java: Internal error in the mapping processor: java.lang.NullPointerException解决

image-20210729121108648

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