Spock單元測試框架實戰指南七 - 動態Mock

這篇講解Spock自帶的mock功能如何和power mock組合使用,發揮更強大的做用html

動態Mock靜態方法 (Spock Where + Power Mock)

上一篇的例子中使用power mock讓靜態方法返回一個指定的值,那能不能每次返回不一樣的值呢?java

咱們先看下什麼場景須要這樣作:app

/**
 * 靜態方法多分支場景
 * @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 | currencycode

這樣的寫法就能夠每次測試業務方法時,讓HttpContextUtils.getCurrentSource()HttpContextUtils.getCurrentCurrency()返回不一樣的來源和幣種,就能輕鬆的覆蓋if和else的分支代碼htm

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端訂單"
}
  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行代碼:

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

文章來源:http://javakk.com/305.html

相關文章
相關標籤/搜索