公司對開發人員的單元測試要求比較高,要求分支覆蓋率、行覆蓋率等要達到60%以上等等。項目中已經集成了jmockit這個功能強大的mock框架,學會使用這個框架勢在必行。從第一次寫一點不會,到完全可以應付工作要求,期間踩了好多坑,學到了不少東西。下面簡單總結一下jmockit這個框架的使用,重點介紹MockUp的使用,因為項目中都采用此種方式模擬方法。
一、框架集成
添加maven依賴
<dependencies> <!-- jmockit必須寫在junit之前 --> <dependency> <groupId>org.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.16</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>
二、@Mocked模擬方式介紹
@Mocked模擬,由錄制、回放、驗證三步驟完成,是對某個類的所有實例的所有方法進行完整的模擬方式。
/** * 被測試類 */ public class App { public String say() { return "Hello World"; } public String say2(){ return "Hello World 2"; } public static String staticSay() { return "Still hello world"; } }
/** * 測試類 */ public class AppTest { /** * 針對類及所有實例的的整體模擬,未寫錄制的方法默認返回0,null等 */ @Mocked App app; @Test public void testSay() { //錄制,定義被模擬的方法的返回值,可以錄制多個行為,寫在一個大括號里也可以,多個大括號隔開也可以 new Expectations() {{ app.say(); result = "say"; }}; //回放,調用模擬的方法 System.out.println(app.say()); //say System.out.println(new App().say()); //say System.out.println(App.staticSay()); //null //驗證 new Verifications() {{ //驗證say模擬方法被調用,且調用了2次 app.say(); times = 2; //驗證staticSay模擬方法被調用,且調用了1次 App.staticSay(); times = 1; }}; } }
三、@Injectable模擬方式介紹
@Injectable和@Mocked的方式很像,區別是@Injectable僅僅對當前實例進行模擬。
/** * 測試類 */ public class AppTest001 { /** * 僅針對當前實例的整體模擬 */ @Injectable App app; @Test public void testSay() { //錄制 new Expectations() {{ app.say(); result = "say"; }}; //回放 System.out.println(app.say()); //say,模擬值 System.out.println(app.say2()); //null,模擬默認值 final App appNew = new App(); System.out.println(appNew.say()); //Hello World,未被模擬,方法實際值 System.out.println(App.staticSay()); //Still hello world,未被模擬,方法實際值 //驗證 new Verifications() { { //驗證say模擬方法被調用 app.say(); times = 1; } { appNew.say(); times = 1; } { //驗證staticSay模擬方法被調用,且調用了1次 App.staticSay(); times = 1; } }; } }
四、Expectations傳參,局部模擬
/** * 測試類 */ public class AppTest002 { @Test public void testSay() { final App app = new App(); //錄制,帶參數表示局部模擬【針對所有實例的局部模擬,具體到某個方法的模擬,其它方法不模擬】 new Expectations(App.class) {{ app.say(); result = "say"; }}; //回放 System.out.println(app.say()); //say,模擬值 System.out.println(app.say2()); //Hello World 2 ,未被模擬,方法實際值 System.out.println(new App().say()); //say,模擬值 System.out.println(App.staticSay()); //Still hello world,未被模擬,方法實際值 } }
五、MockUp局部模擬,可重寫原有方法的邏輯,比較靈活,推薦使用
/** * 測試類 */ public class AppTest003 { @Test public void testSay() { //局部模擬【針對所有實例的局部模擬,具體到某個方法的模擬,其它方法不模擬】 new MockUp<App>(App.class){ @Mock String say(){ return "say"; } }; //回放 System.out.println(new App().say()); //say,模擬值 System.out.println(new App().say2()); //Hello World 2,未被模擬,方法實際值 System.out.println(App.staticSay()); //Still hello world,未被模擬,方法實際值 } }
六、MockUp如何模擬私有方法、靜態方法、靜態塊、構造函數等
1.模擬私有屬性(實例屬性和類屬性),MockUp不支持,采用如下方式
//模擬實例的字段 Deencapsulation.setField(Object objectWithField, String fieldName, Object fieldValue) //模擬類的靜態字段 Deencapsulation.setField(Class<?> classWithStaticField, String fieldName, Object fieldValue)
2.模擬私有方法,MockUp不支持,采用如下方式
//模擬實例方法,注意參數不能為null,如果要傳null請使用帶參數類型的另一個重載方法 Deencapsulation.invoke(Object objectWithMethod, String methodName, Object... nonNullArgs) //模擬類方法 Deencapsulation.invoke(Class<?> classWithStaticMethod, String methodName, Object... nonNullArgs)
3.模擬靜態方法
和模擬實例方法一樣,去掉static即可
4.模擬靜態塊
//mock靜態代碼塊 @Mock void $clinit(Invocation invocation){ }
5.模擬實例塊和構造函數
//mock代碼塊和構造函數 @Mock void $init(Invocation invocation) { }
6.模擬接口,MockUp不支持,采用@Capturing
/** * 被模擬的接口 */ public interface IUserService { String getUserName( ); }
public class UserServiceImpl implements IUserService { @Override public String getUserName() { return "Bob"; } }
/** * 接口模擬測試 */ public class IUserServiceTest { /** * MockUp不能mock接口方法,可以用來生成接口實例 */ @Test public void getUserNameTest001(){ MockUp<IUserService> mockUp = new MockUp<IUserService>(){ @Mock String getUserName( ){ return "Jack"; } }; IUserService obj = new UserServiceImpl(); System.out.println(obj.getUserName()); //Bob,mock失敗 obj = mockUp.getMockInstance(); System.out.println(obj.getUserName()); //Jack,mockUp生成的實例,和自己寫一個接口實現一樣 obj = new UserServiceImpl(); System.out.println(obj.getUserName()); //Bob,mock失敗 } /** * @Capturing 注解可以實現mock接口,所有實現類的實例均被mock * @param base */ @Test public void getUserNameTest002(@Capturing final IUserService base){ IUserService obj = new UserServiceImpl(); System.out.println(obj.getUserName()); //mock成功,返回模擬默認值null //錄制 new Expectations(){ { base.getUserName(); result = "Jack"; } }; System.out.println(obj.getUserName()); //Jack obj = new IUserService() { @Override public String getUserName() { return "Alice"; } }; System.out.println(obj.getUserName()); //Jack } }
七、MockUp模擬方法中調用原方法
/** * 模擬方法調用原方法邏輯測試 */ public class JSONObjectTest { @Test public void getTest(){ JSONObject jsonObject = new JSONObject(); jsonObject.put("a","A"); jsonObject.put("b","B"); jsonObject.put("c","C"); System.out.println(jsonObject.get("a")); //A System.out.println(jsonObject.get("b")); //B System.out.println(jsonObject.get("c")); //C new MockUp<JSONObject>(){ @Mock Object get(Invocation invocation,Object key){ if("a".equals(key)){ return "aa"; }else{ //調用原邏輯 return invocation.proceed(key); } } }; System.out.println(jsonObject.get("a")); //aa System.out.println(jsonObject.get("b")); //B System.out.println(jsonObject.get("c")); //C } }
八、MockUp單元測試用例單個跑正常,批量跑失敗可能的原因
1.測試方法使用了共享變量,相互影響。
2.在一個測試方法里多次MockUp同一個類,將某個類需要mock的方法均寫在一個new MockUp里即可解決,原因未知。
3.在測試方法里的MockUp,在方法結束前,調用一下mockUp.tearDown。