詳解Java 中的UnitTest 和 PowerMock
學(xué)習(xí)一門計(jì)算機(jī)語言,我覺得除了學(xué)習(xí)它的語法外,最重要的就是要學(xué)習(xí)怎么在這個語言環(huán)境下進(jìn)行單元測試,因?yàn)閱卧獪y試能幫你提早發(fā)現(xiàn)錯誤;同時給你的程序加一道防護(hù)網(wǎng),防止你的修改破壞了原有的功能;單元測試還能指引你寫出更好的代碼,畢竟不能被測試的代碼一定不是好代碼;除此之外,它還能增加你的自信,能勇敢的說出「我的程序沒有bug」。
每個語言都有其常用的單元測試框架,本文主要介紹在 Java 中,我們?nèi)绾问褂?PowerMock,來解決我們在寫單元測試時遇到的問題,從 Mock 這個詞可以看出,這類問題主要是解依賴問題。
在寫單元測試時,為了讓測試工作更簡單、減少外部的不確定性,我們一般都會把被測類和其他依賴類進(jìn)行隔離,不然你的類依賴得越多,你需要做的準(zhǔn)備工作就越復(fù)雜,尤其是當(dāng)它依賴網(wǎng)絡(luò)或外部數(shù)據(jù)庫時,會給測試帶來極大的不確定性,而我們的單測一定要滿足快速、可重復(fù)執(zhí)行的要求,所以隔離或解依賴是必不可少的步驟。
而 Java 中的 PowerMock 庫是一個非常強(qiáng)大的解依賴庫,下面談到的 3 個特性,可以幫你解決絕大多數(shù)問題:
1 通過 PowerMock 注入依賴對象
2 利用 PowerMock 來 mock static 函數(shù)
3 輸出參數(shù)(output parameter)怎么 mock
通過 PowerMock 注入依賴對象
假設(shè)你有兩個類,MyService 和 MyDao,MyService 依賴于 MyDao,且它們的定義如下
// MyDao.java
@Mapper
public interface MyDao {
/**
* 根據(jù)用戶 id 查看他最近一次操作的時間
*/
Date getLastOperationTime(long userId);
}
// MyService.java
@Service
public class MyService {
@Autowired
private MyDao myDao;
public boolean operate(long userId, String operation) {
Date lastTime = myDao.getLastOperationTime(userId);
// ...
}
}
這個服務(wù)提供一個 operate 接口,用戶在調(diào)用該接口時,會被限制一個操作頻次,所以系統(tǒng)會記錄每個用戶上次操作的時間,通過 MyDao.getLastOperationTime(long userId) 接口獲取,現(xiàn)在我們要對 MyService 類的 operate 做單元測試,該怎么做?
你可能會想到使用 SpringBoot,它能自動幫我們初始化 myDao 對象,但這樣做卻存在一些問題:
1 SpringBoot 的啟動速度很慢,這會延長單元測試的時間
2 因?yàn)闀r間是一個不斷變化的量,也許這一次你構(gòu)造的時間滿足測試條件,但下一次運(yùn)行測試時,可能就不滿足了。
由于以上原因,我們一般在做單元測試時,不啟動 SpringBoot 上下文,而是采用 PowerMock 幫我們注入依賴,對于上面的 case,我們的測試用例可以這樣寫:
// MyServiceTest.java
@RunWith(PowerMockRunner.class)
@PrepareForTest({MyService.class, MyDao.class})
public class MyServiceTest {
@Test
public void testOperate() throws IllegalAccessException {
// 構(gòu)造一個和當(dāng)前調(diào)用時間永遠(yuǎn)只差 4 秒的返回值
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, -4);
Date retTime = calendar.getTime();
// spy 是對象的“部分 mock”
MyService myService = PowerMockito.spy(new MyService());
MyDao md = PowerMockito.mock(MyDao.class);
PowerMockito
.when(md.getLastOperationTime(Mockito.any(long.class)))
.thenReturn(retTime);
// 替換 myDao 成員
MemberModifier.field(MyService.class, "myDao").set(myService, md);
// 假設(shè)最小操作的間隔是 5 秒,否則返回 false
Assert.assertFalse(myService.operate(1, "test operation"));
}
}
從上面代碼中,我們首先構(gòu)造了一個返回時間 retTime,模擬操作間隔的時間為 4 秒,保證了每次運(yùn)行測試時該條件不會變化;然后我們用 spy 構(gòu)造一個待測試的 MyService 對象,spy 和 mock 的區(qū)別是,spy 只會部分模擬對象,即這里只修改掉 myService.myDao 成員,其他的保持不變。
然后我們定義了被 mock 的對象 MyDao md 的調(diào)用行為,當(dāng) md.getLastOperationTime 函數(shù)被調(diào)用時,返回我們構(gòu)造的時間 retTime,此時測試環(huán)境就設(shè)置完畢了,這樣做之后,你就可以很容易的測試 operate 函數(shù)了。
利用 PowerMock 來 mock static 函數(shù)
上文所說的使用 PowerMock 進(jìn)行依賴注入,可以覆蓋測試中絕大多數(shù)的解依賴場景,而另一種常見的依賴是 static 函數(shù),例如我們自己寫的一些 CommonUtil 工具類中的函數(shù)。
還是使用上面的例子,假設(shè)我們要計(jì)算當(dāng)前時間和用戶上一次操作時間之間的間隔,并使用 public static long getTimeInterval(Date lastTime) 實(shí)現(xiàn)該功能,如下:
// CommonUtil.java
class CommonUtil {
public static long getTimeInterval(Date lastTime) {
long duration = Duration.between(lastTime.toInstant(),
new Date().toInstant()).getSeconds();
return duration;
}
}
我們的 operator 函數(shù)修改如下
// MyService.java
// ...
public boolean operate(long userId, String operation) {
Date lastTime = myDao.getLastOperationTime(userId);
long duration = CommonUtil.getTimeInterval(lastTime);
if (duration >= 5) {
System.out.println("user: " + userId + " " + operation);
return true;
} else {
return false;
}
}
// ...
這里先從 myDao 獲取上次操作的時間,再調(diào)用 CommonUtil.getTimeInterval 計(jì)算操作間隔,如果小于 5 秒,就返回 false,否則執(zhí)行操作,并返回 true。那么我的問題是,如何解掉這里 static 函數(shù)的依賴呢?我們直接看測試代碼吧
// MyServiceTest.java
@PrepareForTest({MyService.class, MyDao.class, CommonUtil.class})
public class MyServiceTest {
// ...
@Test
public void testOperateWithStatic() throws IllegalAccessException {
// ...
PowerMockito.spy(CommonUtil.class);
PowerMockito.doReturn(5L).when(CommonUtil.class);
CommonUtil.getTimeInterval(Mockito.anyObject());
// ...
}
}
首先在注解 @PrepareForTest 中增加 CommonUtil.class,依然使用 spy 對類 CommonUtil 進(jìn)行 mock,如果不這么做,這個類中所有靜態(tài)函數(shù)的行為都會發(fā)生變化,這會給你的測試帶來麻煩。spy 下面的兩行代碼你應(yīng)該放在一起解讀,意為當(dāng)調(diào)用 CommonUtil.getTimeInterval 時,返回 5;這種寫法比較奇怪,但卻是 PowerMock 要求的。至此,你已經(jīng)掌握了 mock static 函數(shù)的技巧。
輸出參數(shù)(output parameter)怎么 mock
有些函數(shù)會通過修改參數(shù)所引用的對象作為輸出,例如下面的這個場景,假設(shè)我們的 operation 是一個長時間執(zhí)行的任務(wù),我們需要不斷輪訓(xùn)該任務(wù)的狀態(tài),更新到內(nèi)存,并對外提供查詢接口,如下代碼:到內(nèi)存,并對外提供查詢接口,如下代碼:
// MyTask.java
// ...
public boolean run() throws InterruptedException {
while (true) {
updateStatus(operation);
if (operation.getStatus().equals("success")) {
return true;
} else {
Thread.sleep(1000);
}
}
}
public void updateStatus(Operation operation) {
String status = myDao.getStatus(operation.getOperationId());
operation.setStatus(status);
}
// ...
上面的代碼中,run() 是一個輪詢?nèi)蝿?wù),它會不斷更新 operation 的狀態(tài),并在狀態(tài)達(dá)到 "success" 時停止,可以看到,updateStatus 就是我們所說的函數(shù),雖然它沒有返回值,但它會修改參數(shù)所引用的對象,所以這種參數(shù)也被稱作輸出參數(shù)。
現(xiàn)在我們要測試 run() 函數(shù)的行為,看它是否會在 "success" 狀態(tài)下退出,那么我們就需要 mock updateStatus 函數(shù),該怎么做?下面是它的測試代碼:
@Test
public void testUpdateStatus() throws InterruptedException {
// 初始化被測對象
MyTask myTask = PowerMockito.spy(new MyTask());
myTask.setOperation(new MyTask.Operation());
// 使用 doAnswer 來 mock updateStatus 函數(shù)的行為
PowerMockito.doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
MyTask.Operation operation = (MyTask.Operation)args[0];
operation.setStatus("success");
return null;
}
}).when(myTask).updateStatus(Mockito.any(MyTask.Operation.class));
Assert.assertEquals(true, myTask.run());
}
上面的代碼中,我們使用 doAnswer 來 mock updateStatus 的行為,相當(dāng)于使用 answer 函數(shù)來替換原來的 updateStatus 函數(shù),在這里,我們將 operation 的狀態(tài)設(shè)置為了 "success",以期待 myTask.run() 函數(shù)返回 true。于是,我們又學(xué)會了如何 mock 具有輸出參數(shù)的函數(shù)了。
以上就是詳解Java 中的UnitTest 和 PowerMock的詳細(xì)內(nèi)容,更多關(guān)于Java UnitTest 和 PowerMock的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
sharding-jdbc 兼容 MybatisPlus動態(tài)數(shù)據(jù)源的配置方法
這篇文章主要介紹了sharding-jdbc 兼容 MybatisPlus動態(tài)數(shù)據(jù)源的配置方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-07-07
Spring Boot 通過注解實(shí)現(xiàn)數(shù)據(jù)校驗(yàn)的方法
這篇文章主要介紹了Spring Boot 通過注解實(shí)現(xiàn)數(shù)據(jù)校驗(yàn)的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
Spring下token過期時間分平臺(web和app)設(shè)置方法
本文詳細(xì)介紹了在Spring環(huán)境下,針對web端和APP端實(shí)現(xiàn)不同token過期時間的方法,通過整合SpringBoot、springSecurity和JWT框架,文章講解了登錄流程、JWT的基本組成以及token鑒權(quán)的核心步驟,需要的朋友可以參考下2024-10-10
mybatis中實(shí)現(xiàn)讓返回值與bean中字段相匹配
這篇文章主要介紹了mybatis中實(shí)現(xiàn)讓返回值與bean中字段相匹配,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
Java實(shí)現(xiàn)excel動態(tài)列導(dǎo)出的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)excel動態(tài)列導(dǎo)出,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03
基于Springboot2.3訪問本地路徑下靜態(tài)資源的方法(解決報錯:Not allowed to load local
這篇文章主要介紹了基于Springboot2.3訪問本地路徑下靜態(tài)資源的方法(解決報錯:Not allowed to load local resource),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08

