學習一門計算機語言,我以爲除了學習它的語法外,最重要的就是要學習怎麼在這個語言環境下進行單元測試,由於單元測試能幫你提前發現錯誤;同時給你的程序加一道防禦網,防止你的修改破壞了原有的功能;單元測試還能指引你寫出更好的代碼,畢竟不能被測試的代碼必定不是好代碼;除此以外,它還能增長你的自信,能勇敢的說出「個人程序沒有bug」。java
每一個語言都有其經常使用的單元測試框架,本文主要介紹在 Java 中,咱們如何使用 PowerMock,來解決咱們在寫單元測試時遇到的問題,從 Mock 這個詞能夠看出,這類問題主要是解依賴問題。git
在寫單元測試時,爲了讓測試工做更簡單、減小外部的不肯定性,咱們通常都會把被測類和其餘依賴類進行隔離,否則你的類依賴得越多,你須要作的準備工做就越複雜,尤爲是當它依賴網絡或外部數據庫時,會給測試帶來極大的不肯定性,而咱們的單測必定要知足快速、可重複執行的要求,因此隔離或解依賴是必不可少的步驟。github
而 Java 中的 PowerMock 庫是一個很是強大的解依賴庫,下面談到的 3 個特性,能夠幫你解決絕大多數問題:spring
假設你有兩個類,MyService
和 MyDao
,MyService
依賴於 MyDao
,且它們的定義以下數據庫
// MyDao.java
@Mapper
public interface MyDao {
/** * 根據用戶 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);
// ...
}
}
複製代碼
這個服務提供一個 operate
接口,用戶在調用該接口時,會被限制一個操做頻次,因此係統會記錄每一個用戶上次操做的時間,經過 MyDao.getLastOperationTime(long userId)
接口獲取,如今咱們要對 MyService
類的 operate
作單元測試,該怎麼作?springboot
你可能會想到使用 SpringBoot,它能自動幫咱們初始化 myDao
對象,但這樣作卻存在一些問題:網絡
因爲以上緣由,咱們通常在作單元測試時,不啓動 SpringBoot 上下文,而是採用 PowerMock 幫咱們注入依賴,對於上面的 case,咱們的測試用例能夠這樣寫:app
// MyServiceTest.java
@RunWith(PowerMockRunner.class)
@PrepareForTest({MyService.class, MyDao.class})
public class MyServiceTest {
@Test
public void testOperate() throws IllegalAccessException {
// 構造一個和當前調用時間永遠只差 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);
// 假設最小操做的間隔是 5 秒,不然返回 false
Assert.assertFalse(myService.operate(1, "test operation"));
}
}
複製代碼
從上面代碼中,咱們首先構造了一個返回時間 retTime
,模擬操做間隔的時間爲 4 秒,保證了每次運行測試時該條件不會變化;而後咱們用 spy
構造一個待測試的 MyService
對象,spy
和 mock
的區別是,spy
只會部分模擬對象,即這裏只修改掉 myService.myDao
成員,其餘的保持不變。框架
而後咱們定義了被 mock 的對象 MyDao md
的調用行爲,當 md.getLastOperationTime
函數被調用時,返回咱們構造的時間 retTime
,此時測試環境就設置完畢了,這樣作以後,你就能夠很容易的測試 operate
函數了。ide
上文所說的使用 PowerMock 進行依賴注入,能夠覆蓋測試中絕大多數的解依賴場景,而另外一種常見的依賴是 static 函數,例如咱們本身寫的一些 CommonUtil
工具類中的函數。
仍是使用上面的例子,假設咱們要計算當前時間和用戶上一次操做時間之間的間隔,並使用 public static long getTimeInterval(Date lastTime)
實現該功能,以下:
// CommonUtil.java
class CommonUtil {
public static long getTimeInterval(Date lastTime) {
long duration = Duration.between(lastTime.toInstant(),
new Date().toInstant()).getSeconds();
return duration;
}
}
複製代碼
咱們的 operator
函數修改以下
// 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
獲取上次操做的時間,再調用 CommonUtil.getTimeInterval
計算操做間隔,若是小於 5 秒,就返回 false
,不然執行操做,並返回 true
。那麼個人問題是,如何解掉這裏 static 函數的依賴呢?咱們直接看測試代碼吧
// 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
進行 mock,若是不這麼作,這個類中全部靜態函數的行爲都會發生變化,這會給你的測試帶來麻煩。spy
下面的兩行代碼你應該放在一塊兒解讀,意爲當調用 CommonUtil.getTimeInterval
時,返回 5;這種寫法比較奇怪,但倒是 PowerMock 要求的。至此,你已經掌握了 mock static 函數的技巧。
有些函數會經過修改參數所引用的對象做爲輸出,例以下面的這個場景,假設咱們的 operation 是一個長時間執行的任務,咱們須要不斷輪訓該任務的狀態,更新到內存,並對外提供查詢接口,以下代碼:
// 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()
是一個輪詢任務,它會不斷更新 operation 的狀態,並在狀態達到 "success"
時中止,能夠看到,updateStatus
就是咱們所說的函數,雖然它沒有返回值,但它會修改參數所引用的對象,因此這種參數也被稱做輸出參數。
如今咱們要測試 run()
函數的行爲,看它是否會在 "success"
狀態下退出,那麼咱們就須要 mock updateStatus
函數,該怎麼作?下面是它的測試代碼:
@Test
public void testUpdateStatus() throws InterruptedException {
// 初始化被測對象
MyTask myTask = PowerMockito.spy(new MyTask());
myTask.setOperation(new MyTask.Operation());
// 使用 doAnswer 來 mock updateStatus 函數的行爲
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
的行爲,至關於使用 answer
函數來替換原來的 updateStatus
函數,在這裏,咱們將 operation
的狀態設置爲了 "success"
,以期待 myTask.run()
函數返回 true
。因而,咱們又學會了如何 mock 具備輸出參數的函數了。
以上代碼只爲了說明應用場景,並不是生產環境級別的代碼,且均經過測試,爲方便後續學習,你能夠在這裏下載:github.com/jieniu/arti…
參考: