公司对开发人员的单元测试要求比较高,要求分支覆盖率、行覆盖率等要达到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.模拟私有属性(实例属性和类属性)
//模拟实例的字段 Deencapsulation.setField(Object objectWithField, String fieldName, Object fieldValue) //模拟类的静态字段 Deencapsulation.setField(Class<?> classWithStaticField, String fieldName, Object fieldValue)
2.模拟私有方法
//模拟实例方法,注意参数不能为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.模拟接口
/** * 被模拟的接口 */ 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里即可解决,原因未知。