单元测试(UT: Unit Test)是保证服务质量的基础。在实际项目的 UT 开发中,我们通常需要执行第三方服务调用、连接数据库等操作,为了让 UT 能够正常运行起来,我们需要执行大量的环境准备工作,这些工作有时比 UT 本身还要费时费力很多,而 mock 机制则能够帮助我们绕开这些必要但不一定要真正需要去做的事情,隔离 UT 与服务的依赖,模拟我们期望的行为和数据。
在 java 生态系统中,有多种 mock 组件可供选择,包括:EasyMock JMock mockito unitils PowerMock JMockit 
jmockit version: 1.44 
junit version: 4.12 
 
 
        
           
      录制(record)、重放(replay)和验证(verify)是 JMockit 的设计哲学(下文如果不做特殊说明,均使用 RRV 进行指代),这三个步骤各自所担负的责任如下:
录制  :录制目标方法的期望行为,依据条件返回期望的数据,或者抛出异常。重放  :当执行测试逻辑时,之前录制的行为会被触发,所谓的重放是应用之前录制的行为。验证  :验证被测试逻辑的实际表现,例如期望的方法是否被调用,实际被调用了几次等。 
实际在日常使用 JUnit 进行 UT 开发时,我们通常也是按照录制、重放、验证这样一个步骤进行的,只不过我们称之为 “Arrange-Act-Assert”,3A 理论与 RRV 本质上是一样的。
下面的代码展示了 RRV 在编写 UT 时的具体框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public  void  test ()           new  Expectations() {         {                      }     };               new  Verifications() {         {                      }     }; } 
使用 JMockit 进行实际 UT 开发时,并不要求同时提供这 3 个步骤实现,一般我们常用的是录制和重放,而业务相关的验证逻辑可以使用断言。
        
           
      Expectations 代码块用于放置录制行为,录制主要分为两种方式:
引用外部 mock 类或接口实例进行录制。 
通过构造方法注入类或对象实例进行录制。 
 
        
           
      1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mocked private  UserService userService;@Test public  void  recordByMockedInstance ()      new  Expectations() {         {             userService.getUserName(anyLong);             result = "zhenchao" ;             userService.getUserAge(anyLong);             result = 28 ;         }     };     Assert.assertEquals("zhenchao" , userService.getUserName(1001L ));     Assert.assertEquals(28 , userService.getUserAge(1002L )); } 
           
      Expectations 提供了带参数的构造方法(定义如下),我们可以将一个类的 Class 对象,或者类实例对象作为参数构造 Expectations 对象,区别在于如果传递的是类 Class 对象则录制会对该类的所有实例生效,如果传递的是类实例对象则仅对当前实例生效。
1 2 3 protected  Expectations (@Nonnull  Object... classesOrObjectsToBePartiallyMocked)    execution = new  RecordAndReplayExecution(this , classesOrObjectsToBePartiallyMocked); } 
以类 Class 对象构造 Expectations 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public  void  recordByClass ()      User user = new  User(1001L );     new  Expectations(User.class) {         {             user.getName();             result = "zhenchao" ;             user.getAge();             result = 28 ;         }     };     Assert.assertEquals("zhenchao" , user.getName());     Assert.assertEquals(28 , user.getAge());          User user2 = new  User(1002L , "zhangsan" , 18 );     Assert.assertEquals("zhenchao" , user2.getName());     Assert.assertEquals(28 , user2.getAge()); } 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public  void  recordByInstance ()      User user = new  User(1001L );     new  Expectations(user) {         {             user.getName();             result = "zhenchao" ;             user.getAge();             result = 28 ;         }     };     Assert.assertEquals("zhenchao" , user.getName());     Assert.assertEquals(28 , user.getAge());          User user2 = new  User(1002L , "zhangsan" , 18 );     Assert.assertEquals("zhangsan" , user2.getName());     Assert.assertEquals(18 , user2.getAge()); } 
           
      如果我们希望目标方法在被调用时,每次返回的期望结果都不一样,那么可以在录制时指定返回一个结果值数组(或列表),数组(或列表)的每个值都对应该方法被调用时期望返回的结果。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public  void  recordSequenceResult ()      User user = new  User(1001L );     new  Expectations(user) {         {             user.getName();             result = new  String[] {"zhangsan" , "lisi" , "wanger" };             user.getAge();             result = new  int [] {16 , 17 , 18 };         }     };          Assert.assertEquals("zhangsan" , user.getName());     Assert.assertEquals(16 , user.getAge());          Assert.assertEquals("lisi" , user.getName());     Assert.assertEquals(17 , user.getAge());          Assert.assertEquals("wanger" , user.getName());     Assert.assertEquals(18 , user.getAge());          Assert.assertEquals("wanger" , user.getName());     Assert.assertEquals(18 , user.getAge()); } 
示例中我们为方法 User#getName 和 User#getAge 录制了一组返回值,然后在相应方法依次被调用时会返回对应下标的返回值,可以看到当第 4 次调用时仍然返回第 3 组值,这是因为没有为第 4 次调用录制期望值,所以继续沿用最后一组期望值。我们也可以使用 returns(v1, v2, ...) 方法来代替 result = (...) 的语法,即 returns("zhangsan", "lisi", "wanger") 等价于 result = new String[] {"zhangsan", "lisi", "wanger"} 语法。
        
           
      上面的示例在录制期望返回值时只设定了一个具体的值,如果期望依据入参条件性设置返回值,可以使用 Delegate 接口。例如下面的示例中我们依据入参 userId 的值,条件性返回具体的用户名称:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public  void  delegate ()      UserService userService = new  UserServiceImpl();     new  Expectations(userService) {         {             userService.getUserName(anyLong);             result = new  Delegate() {                                  public  String delegate (long  userId)                       return  userId > 1003L  ? "unknown"  : "zhenchao" ;                 }             };         }     };     Assert.assertEquals("zhenchao" , userService.getUserName(1001L ));     Assert.assertEquals("zhenchao" , userService.getUserName(1002L ));     Assert.assertEquals("unknown" , userService.getUserName(1004L )); } 
另外一个比较经典的应用场景就是依据入参条件性的选择录制还是委托给目标方法,此时我们需要引入 Invocation 对象。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test public  void  delegate ()      UserService userService = new  UserServiceImpl();     new  Expectations(userService) {         {             userService.getUserInfo(anyLong);             result = new  Delegate() {                                  public  User delegate (Invocation inv, long  userId)                       if  (userId > 1003 ) {                         return  new  User(userId, "u_"  + userId, (int ) (userId % 100 ));                     }                                          return  inv.proceed(userId);                 }             };         }     };     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1001L ), userService.getUserInfo(1001L ));     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1002L ), userService.getUserInfo(1002L ));     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1003L ), userService.getUserInfo(1003L ));     User user = userService.getUserInfo(1004L );     Assert.assertEquals(1004L , user.getId());     Assert.assertEquals("u_1004" , user.getName());     Assert.assertEquals(4 , user.getAge()); } 
示例中我们对于 userId 小于等于 1003 的请求全部委托给目标方法执行。
        
           
      当我们在录制期望时,对于方法的参数设置如果指定了具体的值,那么在重放测试逻辑时只能匹配对应的值,但是很多时候我们并不希望这样精确的匹配。幸运的是 JMockit 对于参数提供了灵活的匹配策略,即 any 语法和 with 语法。
通过 any 语法,我们可以匹配任意类型的参数值,示例:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public  void  any ()  throws  Exception     UserService userService = new  UserServiceImpl();     new  Expectations(userService) {         {                          userService.getUserName(anyLong);             result = "zhenchao" ;         }     };     Assert.assertEquals("zhenchao" , userService.getUserName(RandomUtils.nextLong())); } 
示例中我们使用 anyLong 录制方法匹配任意 long 类型的参数值。此外,JMockit 还提供了以下 any 语法:
1 2 3 4 5 6 7 8 9 10 protected  final  Object any = null ;protected  final  String anyString = new  String();protected  final  Long anyLong = 0L ;protected  final  Integer anyInt = 0 ;protected  final  Short anyShort = 0 ;protected  final  Byte anyByte = 0 ;protected  final  Boolean anyBoolean = false ;protected  final  Character anyChar = '\0' ;protected  final  Double anyDouble = 0.0 ;protected  final  Float anyFloat = 0.0F ;
With 语法相对于 any 语法提供了更加灵活的匹配策略,当 any 无法满足我们的需求时,可以尝试从 with 语法中寻找解决方案。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public  void  with ()  throws  Exception     UserService userService = new  UserServiceImpl();     new  Expectations(userService) {         {             userService.batchUpdateUserInfo(this .withNotNull());             result = 10 ;         }     };     Assert.assertEquals(10 , userService.batchUpdateUserInfo(new  ArrayList<>()));           } 
示例中 UserService#batchUpdateUserInfo 方法接收一个 List<User> 类型的参数,这里我们使用了 withNotNull 方法,期望匹配任何不为 null 入参。此外,JMockit 还提供了以下类型的 with 语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected  final  <T> T with (@Nonnull  Delegate<? super  T> objectWithDelegateMethod)  ;protected  final  <T> T withAny (@Nonnull  T arg)  ;protected  final  <T> T withEqual (@Nonnull  T arg)  ;protected  final  double  withEqual (double  value, double  delta) protected  final  float  withEqual (float  value, double  delta) protected  final  <T> T withInstanceLike (@Nonnull  T object)  ;protected  final  <T> T withInstanceOf (@Nonnull  Class<T> argClass)  ;protected  final  <T> T withNotEqual (@Nonnull  T arg)  ;protected  final  <T> T withNull ()  ;protected  final  <T> T withNotNull ()  ;protected  final  <T> T withSameInstance (@Nonnull  T object)  ;protected  final  <T extends CharSequence> T withSubstring (@Nonnull  T text)  ;protected  final  <T extends CharSequence> T withPrefix (@Nonnull  T text)  ;protected  final  <T extends CharSequence> T withSuffix (@Nonnull  T text)  ;protected  final  <T extends CharSequence> T withMatch (@Nonnull  T regex)  ;
           
      JMockit 在录制期望时提供了 times、minTimes 和 maxTimes 语法,用于限制当前方法被调用的次数。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public  void  times ()  throws  Exception     UserService userService = new  UserServiceImpl();     new  Expectations(userService) {         {             userService.getUserName(anyLong);             result = "zhenchao" ;             times = 1 ;          }     };     Assert.assertEquals("zhenchao" , userService.getUserName(RandomUtils.nextLong()));           } 
           
      Verifications 用于验证被测试方法调用的状态,即方法是否被调用过,调用了多少次等。Verifications 同样提供了 times、minTimes 和 maxTimes 语法,这与 Expectations 中的设置相同。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Mocked private  UserService userService;@Test public  void  verifyForMockedInstance ()      Assert.assertNull(userService.getUserName(1001L ));     Assert.assertNull(userService.getUserName(1001L ));     Assert.assertEquals(0 , userService.getUserAge(1001L ));     new  Verifications() {         {                          userService.getUserName(anyLong);             times = 2 ;                          userService.getUserAge(anyLong);             times = 1 ;         }     }; } @Test public  void  verifyForInstance ()      User user = new  User(1001L , "zhenchao" , 28 );     Assert.assertEquals("zhenchao" , user.getName());     Assert.assertEquals(28 , user.getAge());     Assert.assertEquals(28 , user.getAge());     new  Verifications() {         {                          user.getName();             times = 1 ;                          user.getAge();             times = 2 ;         }     }; } 
           
      如果期望目标方法按照一定的顺序执行,可以使用 VerificationsInOrder 代替 Verifications。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Mocked private  UserService userService;@Test public  void  verifyInOrder ()           Assert.assertNull(userService.getUserName(1001L ));     Assert.assertEquals(0 , userService.getUserAge(1001L ));     new  VerificationsInOrder() {         {                          userService.getUserName(anyLong);             userService.getUserAge(anyLong);         }     }; } 
需要注意的是,VerificationsInOrder 对于局部实例不生效。
        
           
      如果期望所有在重放阶段调用的测试方法都需要被验证,可以使用 FullVerifications 代替 Verifications。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Mocked private  UserService userService;@Test public  void  fullVerify ()           Assert.assertNull(userService.getUserName(1001L ));     Assert.assertEquals(0 , userService.getUserAge(1001L ));     new  FullVerifications() {         {             userService.getUserName(anyLong);         }     }; } 
需要注意的是,FullVerifications 对于局部实例不生效。
        
           
      JMockit 允许在 Verifications 代码块中通过 withCapture 方法捕获重放阶段传递的方法参数,分为 3 种情况:
捕获单次方法调用的参数。 
捕获多次方法调用的参数集合。 
捕获方法调用时新创建的对象。 
 
具体示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Test public  void  capturingSingle ()      userService.checkPassword(1001L , "aaa" );     new  Verifications() {         {                          long  uid;             String pwd;             userService.checkPassword(uid = this .withCapture(), pwd = this .withCapture());             Assert.assertEquals(1001L , uid);             Assert.assertEquals("aaa" , pwd);         }     }; } @Test public  void  capturingMultiple ()      userService.getUserInfo(1001L );     userService.getUserInfo(1002L );     userService.getUserInfo(1003L );     new  Verifications() {         {                          List<Long> userIds = new  ArrayList<>();             userService.getUserInfo(this .withCapture(userIds));             Assert.assertEquals(3 , userIds.size());             Assert.assertEquals(Arrays.asList(1001L , 1002L , 1003L ), userIds);         }     }; } @Test public  void  capturingNewInstances (@Mocked  User user)      userService.updateUserInfo(new  User(1001L , "zhangsan" , 16 ));     userService.updateUserInfo(new  User(1002L , "lisi" , 17 ));     userService.updateUserInfo(new  User(1003L , "wanger" , 18 ));     new  Verifications() {         {                          List<User> users = this .withCapture(new  User(anyLong, anyString, anyInt));             Assert.assertEquals(3 , users.size());             List<User> userInfos = new  ArrayList<>();             userService.updateUserInfo(this .withCapture(userInfos));             Assert.assertEquals(users, userInfos);         }     }; } 
           
      
        
           
      JMockit 提供了多个 mock 注解,用于模拟被测试对象所依赖的运行环境,包括 @Mocked、@Tested、@Injectabe、以及 @Capturing。这些注解可以修饰我们在测试类中声明的 mock 属性  和测试方法的 mock 参数  。被这些注解修饰的属性和参数(类型包括接口、普通类、抽象类、final 类、注解,以及枚举),JMockit 会依据相应注解的语义对其进行实例化。这几个注解的基本语义如下:
@Mocked:委托 JMockit 创建目标接口或者类的实例,所有实例都会被托管(不管是不是 JMockit 创建的),所有方法(包括静态方法、构造方法)均返回对应类型的初始值,如果是引用类型则递归初始化。@Injectabe:委托 JMockit 创建目标接口或者类的实例,仅 JMcokit 创建的实例会被托管,类实例方法(不包括静态方法、构造方法)均返回对应类型的初始值,如果是引用类型则递归初始化。@Tested:标记一个实例是被测试实例,如果能够依据类型推断出用于具体实例化的类,则 JMockit 会自动执行实例化操作,否则需要手动实例化,当我们调用实例的方法时,相应方法并不会被 JMockit 托管,但是配合 @Injectabe 注解,可以注入方法中使用到的依赖实例。@Capturing:委托 JMockit 代理目标接口或者父类,被代理的接口或父类不管子类如何实现(可以是人工编写、匿名类,甚至是动态代理类),其相应的方法都将被 JMockit 托管,适用于代理子类的行为,并且子类是不易描述的。 
        
           
      注解 @Mocked 可以修饰类和接口,其语义是告诉 JMockit 生成当前被 mock 类或接口的对象,该对象的方法(包括静态方法、构造方法)均返回对应类型的初始值,例如对于 int 类型则返回 0,String 类型返回 null,如果返回类型是一个引用类型,则 JMockit 会递归创建该类型的对象,同样该对象的属性都会被设置为相应的初始值。
        
           
      1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mocked private  UserService userService;@Test public  void  mockedInterface ()           Assert.assertNull(userService.getUserName(1001L ));          Assert.assertEquals(0 , userService.getUserAge(1001L ));          User user = userService.getUserInfo(1001L );     Assert.assertNotNull(user);     Assert.assertEquals(0L , user.getId());     Assert.assertNull(user.getName());     Assert.assertEquals(0 , user.getAge()); } 
总结:
注解会创建一个接口的实现类对象。 
返回值为基本类型的方法直接返回初始值。 
返回值为 String 类型的方法直接返回 null。 
返回值为引用类型的方法会递归初始化。 
 
        
           
      1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Mocked private  UserServiceImpl userServiceImpl;@Test public  void  mockedClass ()           Assert.assertNull(userService.getUserName(1001L ));          Assert.assertEquals(0 , userService.getUserAge(1001L ));          User user = userService.getUserInfo(1001L );     Assert.assertNotNull(user);     Assert.assertEquals(0L , user.getId());     Assert.assertNull(user.getName());     Assert.assertEquals(0 , user.getAge());          User newUser = UserServiceImpl.newUserInfo("zhenchao" , 28 );     Assert.assertEquals(0L , newUser.getId());     Assert.assertNull(newUser.getName());     Assert.assertEquals(0 , newUser.getAge());          UserService localService = new  UserServiceImpl();     Assert.assertNull(localService.getUserName(1001L ));     Assert.assertEquals(0 , localService.getUserAge(1001L )); } 
总结:
满足接口类型所有的特点。 
静态方法同样会被托管,效果与实例方法一样。 
即使自己 new 出来的实例也会被托管。 
 
        
           
      注解 @Injectable 同样可以修饰类和接口,其语义是告诉 JMockit 生成当前被 mock 类或接口的对象。相对于 @Mocked 的区别在于,@Injectable 仅托管修饰类或接口的当前实例,而不是所有实例。此外,@Injectable 对类的静态方法、构造方法没有影响。修饰类的示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Injectable private  UserServiceImpl userServiceImpl;@Test public  void  injectableClass ()           Assert.assertNull(userService.getUserName(1001L ));          Assert.assertEquals(0 , userService.getUserAge(1001L ));          User user = userService.getUserInfo(1001L );     Assert.assertNotNull(user);     Assert.assertEquals(0L , user.getId());     Assert.assertNull(user.getName());     Assert.assertEquals(0 , user.getAge());          User newUser = UserServiceImpl.newUserInfo(1004L , "zhenchao" , 28 );     Assert.assertEquals(1004L , newUser.getId());     Assert.assertEquals("zhenchao" , newUser.getName());     Assert.assertEquals(28 , newUser.getAge());          UserService localService = new  UserServiceImpl();     Assert.assertEquals("zhangsan" , localService.getUserName(1001L ));     Assert.assertEquals(18 , localService.getUserAge(1001L )); } 
总结:
注解会创建一个接口的实现类对象,并且仅托管当前创建的实例。 
返回值为基本类型的方法直接返回初始值。 
返回值为 String 类型的方法直接返回 null。 
返回值为引用类型的方法会递归初始化。 
构造方法、静态方法不会被托管。 
 
        
           
      注解 @Tested 一般和 @Injectable 配合使用以 mock 被测试类或接口的对象,如果该对象没有被赋值,则 JMockit 会对该对象进行实例化。实例化过程中如果被测试类具备带参数的构造方法,则 JMockit 会尝试在 mock 属性和 mock 参数中寻找被 @Injectable 修饰的 mock 对象进行注入,否则使用无参构造方法进行实例化,并尝试属性注入。
下面以一个订单示例说明注解 @Tested 和 @Injectable 的配合使用。假设有一个订单服务 OrderService,当调用该服务的 OrderService#submitOrder 方法提交订单时需要调用用户服务 UserService 验证当前提交订单用户的密码,只有在密码验证通过的情况下才会处理订单,并在订单处理成功之后调用邮件服务 EmailService 发送邮件通知用户。订单服务实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public  class  OrderService           private  UserService userService;          @Resource      private  EmailService emailService;     public  OrderService (UserService userService)           this .userService = userService;     }          public  boolean  submitOrder (long  userId, long  orderId, String password)                    if  (userService.checkPassword(userId, password)) {                                       User user = userService.getUserInfo(userId);             emailService.send(user.getEmail(), "submit order success" , "submit order success, orderId: "  + orderId);             System.out.println("user submit order success, userId: "  + userId + ", orderId: "  + orderId);             return  true ;         }         System.out.println("invalid password, userId: "  + userId);         return  false ;     }     public  OrderService setEmailService (EmailService emailService)           this .emailService = emailService;         return  this ;     } } 
在订单服务 OrderService 中,我们设定了通过构造方法注入 UserService 实例,通过属性注入 EmailService 实例,相应的测试方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Tested private  OrderService orderService;@Injectable private  UserService userService;@Test public  void  submitOrder (@Injectable  EmailService emailService)      User zhangsan = new  User(1001L , "zhangsan" , 18 ).setEmail("zhangsan@gmail.com" ).setPassword("aaa" );     new  Expectations() {         {             userService.getUserInfo(zhangsan.getId());             result = zhangsan;             userService.checkPassword(zhangsan.getId(), zhangsan.getPassword());             result = true ;             emailService.send(zhangsan.getEmail(), anyString, anyString);             result = true ;         }     };     Assert.assertTrue(orderService.submitOrder(zhangsan.getId(), RandomUtils.nextLong(), zhangsan.getPassword()));     Assert.assertFalse(orderService.submitOrder(zhangsan.getId(), RandomUtils.nextLong(), "" ));     Assert.assertFalse(orderService.submitOrder(1002L , RandomUtils.nextLong(), zhangsan.getPassword())); } 
我们在测试方法中同时使用了 mock 属性和 mock 参数,并录制了相应服务方法的期望行为,借助 JMockit 可以将这些录制在执行具体测试逻辑时重放。
        
           
      有时候我们只知道父类或接口,但是希望控制它所有的子类(包括人工编写、匿名类,以及动态代理生成等)的行为,这时可以使用 @Capturing 注解。该注解平时虽然较少用到,但在一些场景下还非它不可,尤其是动态代理相关应用场景。
假设我们现在有一个密码校验服务接口 PasswordService,我们并不知道该接口的实现类有哪些,以及如何实现,可能是匿名类,也可能是动态代理,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public  interface  PasswordService      boolean  check (long  userId, String password)  } PasswordService passwordService1 = new  PasswordService() {     @Override      public  boolean  check (long  userId, String password)           return  1001L  != userId && StringUtils.isNotBlank(password);     } }; PasswordService passwordService2 = (PasswordService) Proxy.newProxyInstance(         Thread.currentThread().getContextClassLoader(),         new  Class[] {PasswordService.class},         new  InvocationHandler() {             @Override              public  Object invoke (Object proxy, Method method, Object[] args)  throws  Throwable                  return  args.length == 2                          && (Long) args[0 ] != 1001L  && StringUtils.isNotBlank(String.valueOf(args[1 ]));             }         }); 
假设我们现在想托管所有 PasswordService 接口的实现类,不管是匿名类还是动态生成的,这个时候就可以使用 @Capturing 注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public  void  withCapturing (@Capturing  PasswordService passwordService)      final  long  UID = 1001L ;     new  Expectations() {         {             passwordService.check(UID, anyString);             result = true ;         }     };          Assert.assertTrue(passwordService1.check(UID, "aaa" ));     Assert.assertTrue(passwordService2.check(UID, "bbb" )); } @Test public  void  withoutCapturing ()      final  long  UID = 1001L ;     Assert.assertFalse(passwordService1.check(UID, "aaa" ));     Assert.assertFalse(passwordService2.check(UID, "bbb" )); } 
由示例可以看出,如果目标接口使用 @Capturing 注解修饰,则不管子类如何实现,都不再生效。
注  :1.38 版本存在 bug,对于动态代理生成的类无法代理。
上面的示例也可以使用 MockUp 实现,我们会在后面对 MockUp 的使用进行详细介绍,这里先给出一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public  <T extends PasswordService> void  mockup ()      final  long  UID = 1001L ;     new  MockUp<T>() {         @Mock          public  boolean  check (long  userId, String password)               return  true ;         }     };     PasswordService passwordService1 = new  PasswordService() {         @Override          public  boolean  check (long  userId, String password)               return  false ;         }     };     Assert.assertTrue(passwordService1.check(UID, "aaa" ));     PasswordService passwordService2 = (PasswordService) Proxy.newProxyInstance(             Thread.currentThread().getContextClassLoader(),             new  Class[] {PasswordService.class},             new  InvocationHandler() {                 @Override                  public  Object invoke (Object proxy, Method method, Object[] args)  throws  Throwable                      return  false ;                 }             });     Assert.assertTrue(passwordService2.check(UID, "aaa" )); } 
           
      MockUp 可以伪造目标方法的实现,以返回我们期望的数据或者行为,其语义与 Expectations 相似,但是更加直观和简单。MockUp 适用于一个项目中对一些通用类的 mock 操作,以减少大量重复的 Exceptations 录制代码。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test public  void  mockup ()      new  MockUp<UserService>(UserServiceImpl.class) {         @Mock          public  int  getUserAge (long  userId)               if  (1001L  == userId) {                 return  1 ;             } else  if  (1002L  == userId) {                 return  2 ;             } else  if  (1003L  == userId) {                 return  3 ;             } else  {                 return  -1 ;             }         }     };     UserService userService = new  UserServiceImpl();     Assert.assertEquals(1 , userService.getUserAge(1001L ));     Assert.assertEquals(2 , userService.getUserAge(1002L ));     Assert.assertEquals(3 , userService.getUserAge(1003L ));     Assert.assertEquals(-1 , userService.getUserAge(1004L )); } 
示例中我们通过组合 MockUp 和 @Mock 注解对 UserServiceImpl#getUserAge 方法进行录制。
如果我们希望像 Expectations 那样对目标方法进行条件性 mock,例如依据参数选择是 mock 还是直接调用原始方法,可以引入 Invocation 对象,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Test public  void  mockup ()      new  MockUp<UserService>(UserServiceImpl.class) {         @Mock          public  User getUserInfo (Invocation inv, long  userId)               if  (userId > 1003L ) {                 return  new  User(userId, "u_"  + userId, (int ) userId % 100 );             }                          return  inv.proceed(userId);         }     };     UserService userService = new  UserServiceImpl();     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1001L ), userService.getUserInfo(1001L ));     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1002L ), userService.getUserInfo(1002L ));     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1003L ), userService.getUserInfo(1003L ));     User user = userService.getUserInfo(1004L );     Assert.assertEquals(1004L , user.getId());     Assert.assertEquals("u_1004" , user.getName());     Assert.assertEquals(4 , user.getAge()); } 
可以看到相对于 Expectations 而言,MockUp 的条件性 mock 更加简单。
MockUp 使用简单、直观,与 Expectations 形成互补,从而极大增强了 JMockit 的功能,下面将对 MockUp 的常见应用场景进行举例说明,不过在开始之前我们还是先概括一下 MockUp 在使用上的一些限制:
无法对单个实例进行 mock。 
无法对动态代理类进行 mock。 
当需要对类的所有方法进行 mock 时,开发效率较低。 
 
        
           
      Expectations 在 mock 目标类的私有方法和 native 方法时有些力不从心,这个时候可以使用 MockUp 代替,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public  class  MyClass           public  native  int  nativeMethod ()           private  int  privateMethod ()           return  1 ;     } } @Test public  void  recordByMockUp ()  throws  Exception     new  MockUp<MyClass>(MyClass.class) {         @Mock          public  int  privateMethod ()               return  2019 ;         }         @Mock          public  int  nativeMethod ()               return  2019 ;         }     };     MyClass mc = new  MyClass();     Method privateMethod = MyClass.class.getDeclaredMethod("privateMethod" );     privateMethod.setAccessible(true );     Assert.assertEquals(2019 , privateMethod.invoke(mc));     Assert.assertEquals(2019 , mc.nativeMethod()); } 
           
      前面介绍的 mock 方式,即使目标类被 JMockit 托管,但是在实例化时还是会调用我们自定义的构造方法对类进行实例化,当遇到一些实现复杂或者不规范的类时,可能会在类实例化的过程中执行大量的操作,比如创建数据库连接等,这个时候我们可能希望将构造方法、静态代码块都 mock 调用,以简化 UT 的开发。这个时候我们就可以使用 MockUp 来达到目的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public  class  UserServiceImpl  implements  UserService      private  static  Connection connection;     static  {         try  {                          connection = DBSource.getConnection();         } catch  (SQLException e) {             System.exit(-1 );         }     }     private  int  port;     public  UserServiceImpl ()       }     public  UserServiceImpl (int  port)           this .port = port;     } } @Test public  void  mockupInitialization ()      new  MockUp<UserService>(UserServiceImpl.class) {         @Mock          public  void  $clinit() {                      }         @Mock          public  void  $init(int  port) {                      }     };     UserServiceImpl userService = new  UserServiceImpl(8080 );     Assert.assertEquals(0 , userService.getPort());     Assert.assertNull(UserServiceImpl.getConnection()); } 
示例中目标类 UserServiceImpl 在初始化时需要创建数据库连接,这是一个耗时且易出错的操作,所以我们将其 mock 掉以简化 UT 的编写。在 MockUp 中使用 $clinit 表示类初始化方法,使用 $init 表示类构造方法,之所以这样命名是因为我们的静态代码块在编译成字节码时会组织在一个名为 $clinit 的方法中,而构造方法在编译成字节码时则使用 $init 作为方法名。
        
           
      MockUp 还允许定义另外一类名为 $advice 的方法,用于实现切面增强的目的,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public  void  advice ()      new  MockUp<UserService>(UserServiceImpl.class) {         @Mock          public  Object $advice(Invocation inv) {             long  start = System.nanoTime();             Object result = inv.proceed();             System.out.println("time elapse: "  + (System.nanoTime() - start));             return  result;         }     };     UserService userService = new  UserServiceImpl();     Assert.assertEquals(UserServiceImpl.REGISTRY.get(1002L ), userService.getUserInfo(1002L )); } 
示例中我们在目标方法前后添加了时间打点,用于记录目标方法的执行时间。
        
           
      对于 java 服务端应用来说,Spring 基本上算是必备框架,如果我们希望 mock 被 spring 创建的 bean,可以实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring.xml") public  class  SpringMockTest      @Resource      private  UserService userService;     @Test      public  void  recordByExpectations ()           new  Expectations(userService) {             {                 userService.getUserName(anyLong);                 result = "zhenchao" ;                 userService.getUserAge(anyLong);                 result = 28 ;             }         };         Assert.assertEquals("zhenchao" , userService.getUserName(1001L ));         Assert.assertEquals(28 , userService.getUserAge(1001L ));     }     @Test      public  void  recordByMockup ()           new  MockUp<UserService>(UserServiceImpl.class) {             @Mock              public  String getUserName (long  userId)                   return  "zhenchao" ;             }             @Mock              public  int  getUserAge (long  userId)                   return  28 ;             }         };         Assert.assertEquals("zhenchao" , userService.getUserName(1001L ));         Assert.assertEquals(28 , userService.getUserAge(1001L ));     } } 
示例中分别实现了基于 Expectations 的录制和基于 MockUp 的录制。