這篇講解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深度結合,測試一些特殊的場景,也可以按照這個思路繼續挖掘其他用法