天天看點

Spock單元測試架構系列(六):Mock靜态方法

作者:DonkeyLoveMill

在上一篇中,我們主要介紹了如何使用Spock測試void方法。這一篇中,我們将介紹Spock是如何與PowerMock搭配使用,支援更強大的單元測試的。

在項目開發過程中,我們不可避免的經常會調用一些類的靜态方法來完成某些工作。下面就給出一個業務中經常見到的示例,NumberUtils的formatNumber靜态方法可以對傳入的銷量值,按照業務的要求進行格式化操作。

public class NumberUtils {

    private static final long MILLION = 1000000L;
    private static final long TEN_THOUSAND = 10000L;

    
    public static String formatNumber(long value) {
        // >100萬,顯示100萬+;  > 1萬,傳回N萬+;
        if (value >= MILLION) {
            return "100萬+";
        } else if (value >= TEN_THOUSAND) {
            return value / 10000 + "萬+";
        } else {
            return String.valueOf(value);
        }
    }
}           

我們有一個ItemService類,它有一個方法将ItemDO對象轉化成ItemVO對象,其中就用到了 NumberUtils 類的 formatNumber靜态方法。

@Service
public class ItemService {

    // 其他業務邏輯
    ...

    public ItemVO convertItemDo2Vo(ItemDO itemDO) {
        ItemVO itemVO = new ItemVO();
        itemVO.setItemId(itemDO.getItemId());
        itemVO.setItemTitle(itemDO.getItemTitle());
        itemVO.setSaleCountDesc(NumberUtils.formatNumber(itemDO.getSaleCount()));
        itemVO.setItemPicture(itemDO.getItemPicture());
        return itemVO;
    }

    // 其他業務邏輯
	...

}           

為了僅測試ItemService自身的行為,我們需要将NumberUtils的formatNumber靜态方法Mock掉。但Spock作為一款groovy 編寫的單測架構,僅支援簡單的mock,對靜态類等的mock也隻是對groovy的類起作用,缺乏對java靜态類、靜态方法等mock的支援,有必要引入其他更強大的單測架構,這就是本文要提到的PowerMock。

PowerMock是一款對其他單測架構進行增強的架構。提供了靜态類、靜态方法、私有方法、構造函數、final變量等的mock方法,可謂功能強大。

引入PowerMock

<dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>3.12.4</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-core</artifactId>
      <version>2.0.9</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>2.0.9</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>2.0.9</version>
      <scope>test</scope>
    </dependency>           

使用

Mock靜态方法

引入PowerMock之後,針對上面給出的業務代碼示例,我們通過Spock+PowerMock的方式測試。

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([NumberUtils.class])
class ItemServiceTest extends Specification {

    def itemService = new ItemService()

    void setup() {
        // mock 靜态類
        PowerMockito.mockStatic(NumberUtils.class)
    }

    @Unroll
    def "test convertItemDo2Vo" () {
        given:
        def itemDO = new ItemDO(
            itemId: 12345L,
            itemTitle: "test",
            saleCount: 100L,
            itemPicture: "test picture",
        )
        and: "mock 靜态方法傳回"
        PowerMockito.when(NumberUtils.formatNumber(Mockito.anyLong()))
            .thenReturn("100萬+")

        when:
    	def itemVO = itemService.convertItemDo2Vo(itemDO)
        then:
        with(itemVO) {
            saleCountDesc == "100萬+"
        }
    }
}           

Spock的單測代碼是繼承自Specification基類,而Specification又是基于Junit的注解@RunWith()實作的。

PowerMock的PowerMockRunner也是繼承自Junit,是以使用PowerMock的@PowerMockRunnerDelegate()注解可以指定Spock的父類Sputnik去代理運作PowerMock,這樣就可以在Spock裡使用PowerMock去模拟靜态方法、final方法和私有方法等。

@PrepareForTest([NumberUtils.class]) 指定我們将要代理的靜态類。然後,在setup()方法裡面Mock我們的靜态類:PowerMockito.mockStatic(NumberUtils.class)

最後,我們通過 PowerMockito.when(NumberUtils.formatNumber(Mockito.anyLong())).thenReturn("100萬+") 指定NumberUtils的formatNumber的傳回值為 "100萬+"

動态Mock靜态方法

使用PowerMock可以讓靜态方法傳回一個指定的值,也是可以每次傳回不同的值。我們給上面的業務代碼增加一些邏輯,根據上下文環境的不同,給商品打上不同的類型。

@Service
public class ItemService {

    // 其他業務邏輯
    ...

    public ItemVO convertItemDo2Vo(ItemDO itemDO) {
        ItemVO itemVO = new ItemVO();
        itemVO.setItemId(itemDO.getItemId());
        itemVO.setItemTitle(itemDO.getItemTitle());
        itemVO.setSaleCountDesc(NumberUtils.formatNumber(itemDO.getSaleCount()));
        itemVO.setItemPicture(itemDO.getItemPicture());

        // 新增 打标邏輯
        if ("A".equals(ContextUtils.getSource())) {
            itemVO.setType(1);
        } else if ("B".equals(ContextUtils.getSource())) {
            itemVO.setType(2);
        }
        
        return itemVO;
    }
	
    // 其他業務邏輯
	...

}           

針對新增邏輯,增加單元測試代碼,為了友善的覆寫if-else邏輯,我們Mock ContextUtils.getSource() 靜态方法傳回不同的值。

Spock的where标簽可以友善的和PowerMock結合使用,讓PowerMock模拟的靜态方法每次傳回不同的值。

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([NumberUtils.class])
class ItemServiceTest extends Specification {

    def itemService = new ItemService()

    void setup() {
        // mock 靜态類
        PowerMockito.mockStatic(NumberUtils.class)
    }

    @Unroll
    def "test convertItemDo2Vo" () {
        given:
        def itemDO = new ItemDO(
            itemId: 12345L,
            itemTitle: "test",
            saleCount: 100L,
            itemPicture: "test picture",
        )
        and: "mock 靜态方法傳回"
        PowerMockito.when(NumberUtils.formatNumber(Mockito.anyLong()))
            .thenReturn("100萬+")
        PowerMockito.when(ContextUtils.getSource()).thenReturn(currentSource)

        when:
        def itemVO = itemService.convertItemDo2Vo(itemDO)
        then:
        with(itemVO) {
            saleCountDesc == "100萬+"
            type == expectedType
        }
        where:
        currentSource || expectedType
        "A"           || 1
        "B"           || 2 
    }
}           

PowerMock的thenReturn方法傳回的值 是 currentSource 變量,并非具體的值。我們通過where标簽來枚舉不同的測試用例。

以上代碼示例僅為友善示範,都非常簡單。實際業務中我們可能會遇到各種各種非常複雜的靜态方法,也會有各種複雜的if-else分支條件,通過Spock+PowerMock的結合,讓我們可以更加得心應手的應對這些問題,提升我們的單測效率。

繼續閱讀