Java 中的 UnitTest 和 PowerMock

UnitTest 和 PowerMock

學習一門計算機語言,我以爲除了學習它的語法外,最重要的就是要學習怎麼在這個語言環境下進行單元測試,由於單元測試能幫你提前發現錯誤;同時給你的程序加一道防禦網,防止你的修改破壞了原有的功能;單元測試還能指引你寫出更好的代碼,畢竟不能被測試的代碼必定不是好代碼;除此以外,它還能增長你的自信,能勇敢的說出「個人程序沒有bug」。java

每一個語言都有其經常使用的單元測試框架,本文主要介紹在 Java 中,咱們如何使用 PowerMock,來解決咱們在寫單元測試時遇到的問題,從 Mock 這個詞能夠看出,這類問題主要是解依賴問題。git

在寫單元測試時,爲了讓測試工做更簡單、減小外部的不肯定性,咱們通常都會把被測類和其餘依賴類進行隔離,否則你的類依賴得越多,你須要作的準備工做就越複雜,尤爲是當它依賴網絡或外部數據庫時,會給測試帶來極大的不肯定性,而咱們的單測必定要知足快速、可重複執行的要求,因此隔離或解依賴是必不可少的步驟。github

而 Java 中的 PowerMock 庫是一個很是強大的解依賴庫,下面談到的 3 個特性,能夠幫你解決絕大多數問題:spring

  1. 經過 PowerMock 注入依賴對象
  2. 利用 PowerMock 來 mock static 函數
  3. 輸出參數(output parameter)怎麼 mock

經過 PowerMock 注入依賴對象

假設你有兩個類,MyServiceMyDaoMyService 依賴於 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 對象,但這樣作卻存在一些問題:網絡

  1. SpringBoot 的啓動速度很慢,這會延長單元測試的時間
  2. 由於時間是一個不斷變化的量,也許這一次你構造的時間知足測試條件,但下一次運行測試時,可能就不知足了。

因爲以上緣由,咱們通常在作單元測試時,不啓動 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 對象,spymock 的區別是,spy 只會部分模擬對象,即這裏只修改掉 myService.myDao 成員,其餘的保持不變。框架

而後咱們定義了被 mock 的對象 MyDao md 的調用行爲,當 md.getLastOperationTime 函數被調用時,返回咱們構造的時間 retTime,此時測試環境就設置完畢了,這樣作以後,你就能夠很容易的測試 operate 函數了。ide

利用 PowerMock 來 mock static 函數

上文所說的使用 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 函數的技巧。

輸出參數(output parameter)怎麼 mock

有些函數會經過修改參數所引用的對象做爲輸出,例以下面的這個場景,假設咱們的 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…

參考:

相關文章
相關標籤/搜索