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端訂單"
}
  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM