在上一篇中,我們主要介紹了如何使用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的結合,讓我們可以更加得心應手的應對這些問題,提升我們的單測效率。