單元測試mock框架——jmockit實戰


JMockit是google code上面的一個java單元測試mock項目,她很方便地讓你對單元測試中的final類,靜態方法,構造方法進行mock,功能強大。項目地址在:http://jmockit.googlecode.com http://jmockit.org/。詳細用法可以在上面找到答案。

JMockit的測試方式可以通過下面2個途徑實現

一.根據用例的測試路徑,測試代碼內部邏輯

        對於這種情景,可以使用jmockit的基於行為的mock方式。在這種方式中,目的是測試單元測試及其依賴代碼的調用過程,驗證代碼邏輯是否滿足測試路徑。  由於被依賴代碼可能在自己單測中已測試過,或者難以測試,就需要把這些被依賴代碼的邏輯用預定期待的行為替換掉,也就是mock掉,從而把待測是代碼隔離開,這也是單元測試的初衷。 這種方式和白盒測試接近。

二.根據測試用例的輸入輸出數據,測試代碼是否功能運行正常。

        對於這種情景,可以使用jmockit基於狀態的mock方式。目的是從被測代碼的使用角度出發,結合數據的輸入輸出來檢驗程序運行的這個正確性。使用這個方式,需要把被依賴的代碼mock掉,實際上相當於改變了被依賴的代碼的邏輯。通常在集成測試中,如果有難以調用的外部接口,就通過這個方式mock掉,模擬外部接口。 這種方式有點像黑盒測試。

下面根據一個簡單例子簡單介紹JMockit的幾個常用測試場景和使用方法。

       被測試類:一個猜骰子點數的類。new Guess(int n)時候指定最大猜數次數,並且生成實際點數。在n次猜測內猜中則輸出成功,n次猜測失敗后通過failHandle()輸出錯誤。結果輸出通過GuessDAO來保存。但GuessDAO還沒實現。


/** 在n次機會隨機猜骰子點數 ,結果保存到數據庫中 */
public class Guess {
private int maxTryTime; // 最大重試次數
private int tryTime = 0; // 當前重試次數
private int number = (int) (Math.random() * 6); // 目標數字
private GuessDAO guessDAO; // 持久化依賴
public Guess(int maxRetryTime) {
this.maxTryTime = maxRetryTime;
}
public void doit() {
while (tryTime++ < maxTryTime && !tryIt()) {
// 到達最大嘗試次數仍不成功則調用handle
if (tryTime == maxTryTime) {
failHandle();
}
}
}
public boolean tryIt() { // 最壞情況下調用maxRetryTime次tryIt(),猜中則保存信息
if (number == randomGuess()) {
guessDAO.saveResult(true, number);
return true;
}
return false;
}
public void failHandle() { // 失敗處理,猜不中時調用
guessDAO.saveResult(false, number);
}
private int randomGuess(){ // 猜的隨機過程
return (int) (Math.random() * 6);
}
public void setGuessDAO(GuessDAO guessDAO) {
this.guessDAO = guessDAO;
}
}
下面通過3個測試用例來說明如何使用jmockit

以下代碼基於jmockit1.0 左右版本,新版去廢除了一些功能(如@Mocked不能修飾成員)

1. 測試當沒有一次猜中時,代碼邏輯如何執行。

先上測試代碼:

public class GuessTest {

@Tested // 表明被修飾實例是將會被自動構建和注入的實例
Guess guess = new Guess(3);
@Injectable // 表明被修飾實例將會自動注入到@Tested修飾的實例中,並且會自動mock掉,除非在測試前被賦值
GuessDAO guessDAO;

/**
* 連續3次失敗
*/
@Test
public void behaviorTest_fail3time() {

new Expectations() { // Expectations中包含的內部類區塊中,體現的是一個錄制被測類的邏輯。
@Mocked(methods="tryIt") // 表明被修飾的類對tryIt()方法進行mock。
Guess g;
{
g.tryIt(); // 期待調用Guess.tryIt()方法
result = false; // mock掉返回值為false(表明猜不中)
times = 3; // 期待以上過程重復3次
guessDAO.saveResult(false, anyInt); // 期待調用guessDAO把猜失敗的結果保存
}
};
guess.doit(); // 錄制完成后,進行實際的代碼調用,也稱回放(replay)
}
}
說明下這個測試代碼的目的: 測試行為是guess.doit(),代碼期望在調用doit()函數后,會發生:
        1.調用tryIt,並把結果mock為false;
        2.重復第一步3次;
        3.把結果通過guessDAO保存。即調用3次均猜錯數字

        可以看出,JMockit在基於行為的測試中,體現3個步驟。第一個是腳本錄制,也就是把期望的行為記錄下來。在上面例子中,在Expectation內部類的區塊中的代碼就是期待發生的行為。第二是回放,也就是guess.doit()觸發的過程。第三是檢驗,在這里沒有確切體現出,但是的確發生着檢驗:假設doit方法調用后,代碼的邏輯沒有符合錄制過程中的腳本的行為,那么測試結果失敗(其實Jmockit有專門的Verifications做檢驗,但是這里Expecation已經包含了這個功能,如果用NonStrictExpecation就需要有檢驗塊)。

        再介紹下這段代碼中用到的各個JMockit元素(結論源自文檔及自己代碼測試):

@Tested和@Injectable: 對@Tested對象判斷是否為null,是則通過合適構造器初始化,並實現依賴注入。調用構造方法時,會嘗試使用@Injectable的字段進行構造器注入。普通注入時,@Injectable字段如果沒有在測試方法前被賦值,其行為將會被mock成默認值(靜態方法和構造函數不會被mock掉)。Injectable最大作用除了注入,還有就是mock的范圍只限當前注釋實例。一句話:@Injectable的實例會自動注入到@Tested中,如果沒初始賦值,那么JMockit將會以相應規則初始化。

@Mocked:@Mocked修飾的實例,將會把實例對應類的所有實例的所有行為都mock掉(無論構造方法,還是private,protected方法,夠霸氣吧)。在Expectation區塊中,聲明的成員變量均默認帶有@Mocked,但是本例沒有省略,是因為@Mocked會mock掉所有方法,而回放的代碼中doit函數我們是不希望它也被mock,所以通過method="tryIt"來設置被mock的類只對tryIt方法進行mock。

Expectations:這是錄制期望發生行為的地方。result和times都是其內定成員變量。result可以重定義錄制行為的返回值甚至通過Delegate來重定義行為,times是期待錄制行為的發生次數。在Expectations中發生的調用,均會被mock。由於沒定義result,所以guessDAO.saveResult()調用的結果返回空。

2. 當多次失敗后,最后一次猜數成功時,代碼邏輯如何執行。

在上面的測試代碼中,加多一個測試方法:


/**
* 兩次失敗,第三次猜數成功
*/
@Test
public void behaviorTest_sucecess() {

new Expectations(Guess.class) { // 構造函數可以傳入Class或Instance實例
{
guess.tryIt();
result = false;
times=2;
invoke(guess, "randomGuess", new Object[]{}); // invoke()能調用私有方法
result = (Integer)getField(guess, "number"); // getField()能操作私有成員
guessDAO.saveResult(true, (Integer)getField(guess, "number"));
}
};
guess.doit();
}
        第二個測試用例是期待先猜2次失敗,第3次猜中。

        所以錄制中會先調用2次tryIt並返回false,在發生第3次調用時,通過invoke調用私有方法randomGuess,並期待其返回被測實例的私有成員number,通過這種作弊的方式,自然肯定能在第三次猜中數字。最后期待guessDAO把結果保存。

        這段代碼和之前的區別是,在Expectation中沒定義成員變量,而把Guess.class顯式地通過構造函數傳入。這么做也是為了只對tryIt方法mock,因為在Expectation構造函數傳入Class對象或Instance對象后,只會區塊內Class或Instance對應的行為進行mock。
通過以上2個基於行為mock的例子,應該對JMockit如何測試代碼內部邏輯有了解了吧。下面再對基於狀態的mock介紹:

3. 模擬正常猜骰子,觀察輸出猜中的概率

再加入第三各測試方法:


/**
* 模擬正常執行,計算抽中概率,把DAO mock掉
*/
@Test
public void stateTest_mockDAO() {
final Map<Integer, Integer> statMap = new HashMap<Integer, Integer>(); // statMap.get(0)為猜中次數,statMap.get(1)為失敗次數
statMap.put(0, 0);
statMap.put(1, 0);
guessDAO = new MockUp<GuessDAO>() { // MockUp用來定義新的代碼邏輯
@SuppressWarnings("unused")
@Mock
public boolean saveResult(boolean isSuccess, int n) {
if (isSuccess) {
statMap.put(0, statMap.get(0)+1);
System.out.println("you guess it! dice:" + n);
} else {
statMap.put(1, statMap.get(1)+1);
System.out.println("you didn't guess it. dice:" + n);
}
return true;
}
}.getMockInstance();

for (int i=0; i<1000; i++) {
Guess guess = new Guess(3);
guess.setGuessDAO(guessDAO);
guess.doit();
}
System.out.println("hit" + statMap.get(0));
System.out.println("not hit" + statMap.get(1));
double rate =((double) statMap.get(0)) / (statMap.get(0)+statMap.get(1));
System.out.println("hit rate=" + rate);
}
        第三個用例目的是,測試在指定嘗試次數下猜中數字的概率。這就不再盯着代碼內部邏輯,而從整體功能角度進行測試,把內部無法調用的的依賴接口mock掉。

        在基於狀態的mock中,看不到了Expectations,@Mocked等字樣了。取而代之的是MockUp,@Mock。
        代碼中對GuessDAO的保存方法進行了重定義。讓其直接從控制帶輸出消息。

        通過這種方式,不僅可以進行功能黑盒測試,還可以盡快地讓測試代碼跑起來。

        MockUp中的泛型類型是被重定義的類,重定義的方法需要和原類中的方法簽名一致。但是,static方法可以省區static關鍵字。如:

new MockUp<Calendar>() {
@Mock
public Calendar getInstance() {
return calendar1;
}
};

        至此,通過三個例子,把JMockit的2個測試方式簡單介紹了。但是JMockit的功能不僅如此,詳細能請查看官方文檔和實例。

=============

過程中遇到還未解決的疑問:

1. 基於行為的mock,需要對回放的類的具體類型類mock,沒法針對父類類型mock?

Guess g = new Guess(3);
new Expectations() {
@Mocked(methods="tryIt")
GuessParent mg; // 對Guess父類進行mock
{
mg.tryIt();result=true;
}
};
g.doit();
假如聲明的mg類型是Guess的父類,則回放中調用Guess.doit()將不能捕捉道mg.tryIt();導致測試失敗。
除非在expectation構造函數傳入實例g才可以。
2. 基於行為的動態mock, 文檔說: If the Class object for a given class is passed, the methods and constructors defined in that class are considered for mocking,but not the methods and constructors of its super-classes. If an instance of a given class is passed, then all methods defined in the whole class hierarchy, from the concrete class of the given instance up to (but not including) Object, are considered for mocking; the constructors of these classes, however, are not (since an instance was already created). 粗體的不是很理解,是說mock的父類的方法和構造函數不被mock?但測試結果卻不是這樣

其他Mock框架與jmockit對比

---------------------
作者:ultrani
來源:CSDN
原文:https://blog.csdn.net/ultrani/article/details/8993364?utm_source=copy
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM