Bootstrap

Jmockit使用详解之Mocking

Jmockit使用详解之Mocking

简介

翻译自官方文档

Jmockit主要有两个部分组成:Mocking(也称“Expectation”)和Faking,Faking下一章会讲。实际上大部分测试中,Faking用的比Mocking要多。

Mocking有一套流程:Exceptations, replay,和Verfications,Exceptations也就是期望,它会记录期望行为,常常就是我们希望伪造的部分;replay是回放,是调用我们想测试的代码的时候;Verfications是验证(可以使用各种assert),它的语法和Exceptations类似,是用来验证我们的最后的结果符不符合期望的逻辑。

我的理解:

mocking关注输入输出,对于给定输入,moking能够产生期望的输出,它不倾向去更改mock对象的内部实现来实现期望输出

faking常常针对某个方法或某个类的内部实现进行伪造(内部搞破坏),通过改变部分代码的运行逻辑产生期望的结果

Jmockit能够mock public方法,final,static方法方法和构造函数。当mock一个对象后,在原对象运行的时候将会使用mock后的对象来代替运行。

一个案例

假设现在有一个sevice类,需要完成这些操作:

  1. 查询数据库
  2. 数据入库
  3. 发邮件通知

对于LLT(low level test)来说,我们需要验证程序的内部逻辑是否正确,在操作数据库、发邮件时只需要得到正确的反馈(访问数据库、发邮件都能操作一般都是依赖第三方,这些第三方的东西一般都是假定正确的),而不需关注(通常也没法配置,因为环境是变化的)要连接哪个数据库,给哪个地址发邮件等操作,因此这些跟我们程序的内部逻辑不是很相关或者需要依赖真实环境的部分可以mock掉。

这个案例发邮件是mock的,至于实体类和数据配置以及操作可以忽略。

service类:

public final class MyBusinessService
{
   private final EntityX data;
   public MyBusinessService(EntityX data) { this.data = data; }
   public void doBusinessOperationXyz() throws EmailException
   {
      List<EntityX> items = find("select item from EntityX item where item.someProperty=?1", data.getSomeProperty());

      BigDecimal total = new BigDecimal("12.30");
      data.setTotal(total);
      persist(data);
      sendNotificationEmail(items);
   }
   private void sendNotificationEmail(List<EntityX> items) throws EmailException
   {
      Email email = new SimpleEmail();
      email.setSubject("Notification about processing of ...");
      email.addTo(data.getCustomerEmail());
      String message = buildNotificationMessage(items);
      email.setMsg(message);

      email.send();
   }
   private static String buildNotificationMessage(List<EntityX> items)
   {
      StringBuilder message = new StringBuilder();

      for (EntityX item : items) {
         message.append(item.getSomeProperty()).append(" Total: ").append(item.getTotal());
      }
      return message.toString();
   }
}

主要的测试代码

@Test注解负责正确的初始化相关对象,有点像Spring的@Component

@Mocked 注解标识相关的类将会被Mock掉

Verifications 表示验证阶段,例如,{ anyEmail.send(); times = 1; }表示上面代码运行过程中,anyEmail.send运行了一次

Expectations 表示期望,可以让我们mock的对象产生期望的运行结果。这能够帮助我们在测试在不同的条件(例如是否出现异常)下代码逻辑的正确性。

public final class MyBusinessServiceTest
{
   @Rule public final ExpectedException thrown = ExpectedException.none();

   @Tested final EntityX data = new EntityX(1, "abc", "[email protected]");
   @Tested(fullyInitialized = true) MyBusinessService businessService;
   @Mocked SimpleEmail anyEmail;
   @Test
   public void doBusinessOperationXyz() throws Exception
   {
      EntityX existingItem = new EntityX(1, "AX5", "[email protected]");
      persist(existingItem);//持久化,这里没使用Mock
      //被测试代码
      businessService.doBusinessOperationXyz();
      assertNotEquals(0, data.getId()); // implies "data" was persisted
      new Verifications() {{ anyEmail.send(); times = 1; }};
   }
   @Test
   public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception
   {
      String email = "invalid address";
      data.setCustomerEmail(email);Expectations 
      new Expectations() {{ anyEmail.addTo(email); result = new EmailException(); }};
      thrown.expect(EmailException.class);
      businessService.doBusinessOperationXyz();
   }
}

Mocking

jmockit中Expectations Api 主要提供了mocking的功能,mocking主要关注代码的行为,用于测试比较独立的两个模块间的交互功能,也就是当一个unit(一个类或者模块)依赖另一个unit时,我们可以将另一个unit mock掉。

Mock的类型和实例

方法以及构造器是moking主要的对象。主要有两种moking对象的声明形式:作为一个类的属性、作为方法中的一个参数(传参)

mock的type(类型)可以是接口,类以及注解或者枚举。在测试阶段,mocked type的所有非私有方法和所有非私有构造器都会被mock,如果mock的类型是一个类,它的所有父类(不包括Object)也会被mock。

当一个方法或构造器被mock,在测试的时候,具体的实现将会有Jmockit掌管。

案例代码如下:

若mock的对象作为方法的参数,该对象将会由Jmockit来创建,然后传递给JUunit等test runner,所以参数不能为nulll

若mock的对象作为属性变量,Jmocki将会创建后赋值给这个变量,前提是它不能为final

// "Dependency" is mocked for all tests in this test class.
// The "mockInstance" field holds a mocked instance automatically created for use in each test.
//该mock对象作用于整个测试类
@Mocked Dependency mockInstance;

@Test
//作为方法的参数传入Mock对象
public void doBusinessOperationXyz(@Mocked AnotherDependency anotherMock) {
   ...
   new Expectations() {{ // an "expectation block"
      ...
      // Record an expectation, with a given value to be returned:
      //记录期望(调用某个方法产生期望的输出)
      mockInstance.mockedMethod(...); result = 123;
      ...
   }};
   ...
   // Call the code under test.
   // 验证阶段,就是调用要测试的代码
   ...
   new Verifications() {{ // a "verification block"
      // Verifies an expected invocation:
      anotherMock.save(any); times = 1;
   }};
   ...
}

相关注解的差异:

@Mocked 主要的mocking注解,可以带一个属性stubOutClassInitialization

@Injectable 单例,只会mock一个实例的实例方法

@Capturing 将会mock所有实现该mock接口的类

期望(Expectations)

期望中一个方法可以出现多次,实际运行中的方法匹配expectation不仅是看方法签名,也看传递的参数的值是否一致。

@Test
public void doBusinessOperationXyz(@Mocked Dependency mockInstance) {
   ...
   new Expectations() {{
      ...
      // An expectation for an instance method:
      mockInstance.someMethod(1, "test"); result = "mocked";
      ...
   }};

   // A call to code under test occurs here, leading to mock invocations
   // that may or may not match specified expectations.
}

record-replay-verify 模型

测试过程可以被分为三个阶段:

准备阶段:初始化相关的类

测试代码执行阶段

测试结果验证阶段

但是在基于(期望的)行为的测试中,我们可以将测试分为:

记录阶段: 所谓记录就是记录我们mock的对象的行为,实际上就是上面的期望(记录不如说是期望)

回放阶段: 也就是代码实际执行阶段(回放我们上面的记录)

验证阶段: 验证代码是否按照期望进行运行(各种assert)

下面是几种常见rrv模型出现的形式:

public class SomeTest
{
   // Zero or more "mock fields" common to all test methods in the class:
   //作用于整个类
   @Mocked Collaborator mockCollaborator;
   @Mocked AnotherDependency anotherDependency;
   ...

   @Test
   public void testWithRecordAndReplayOnly(mock parameters) {
      // Preparation code not specific to JMockit, if any.

      new Expectations() {{ // an "expectation block"
         // One or more invocations to mocked types, causing expectations to be recorded.
         // Invocations to non-mocked types are also allowed anywhere inside this block
         // (though not recommended).
      }};

      // Code under test is exercised.

      // Verification code (JUnit/TestNG assertions), if any.
   }

   @Test
   public void testWithReplayAndVerifyOnly(mock parameters) {
      // Preparation code not specific to JMockit, if any.

      // Code under test is exercised.
      //这里没有Expectations也行
      new Verifications() {{ // a "verification block"
         // One or more invocations to mocked types, causing expectations to be verified.
         // Invocations to non-mocked types are also allowed anywhere inside this block
         // (though not recommended).
      }};

      // Additional verification code, if any, either here or before the verification block.
   }

   @Test
   public void testWithBothRecordAndVerify(mock parameters) {
      // Preparation code not specific to JMockit, if any.

      new Expectations() {{
         // One or more invocations to mocked types, causing expectations to be recorded.
      }};

      // Code under test is exercised.

      new VerificationsInOrder() {{ // an ordered verification block
         // One or more invocations to mocked types, causing expectations to be verified
         // in the specified order.
      }};

      // Additional verification code, if any, either here or before the verification block.
   }
}

测试类的初始化和注入

@Tested 注解的实例属性将会被自动初始化和注入,如果在测试方法执行前仍为null,将会调用默认构造函数(可以保证不为null)

@Injectable 对象的注入,可以不是mock对象

@Mocked或者@Capturing不会进行对象注入

public class SomeTest
{
   @Tested CodeUnderTest tested;
   @Injectable Dependency dep1;
   @Injectable AnotherDependency dep2;
   //需要指明初始值,不然将会使用默认值
   @Injectable int someIntegralProperty = 123;

   //可以通过注解来赋值
   @Test
   public void someTestMethod(@Injectable("true") boolean flag, @Injectable("Mary") String name) {
      // Record expectations on mocked types, if needed.

      tested.exerciseCodeUnderTest();

      // Verify expectations on mocked types, if required.
   }
}

Record

记录阶段可以记录返回值(result=),在回放阶段,相应的方法被调用后就会返回记录的值

如果希望有异常出现可以用result赋值

result多次赋值表示可以返回多个结果(依次的,不是同时返回)

也可以用return(value1,value2)的形式来返回多个值

public class ClassUnderTest
{
(1)private final DependencyAbc abc = new DependencyAbc();

   public void doSomething() {
(2)   int n = abc.intReturningMethod();

      for (int i = 0; i < n; i++) {
         String s;

         try {
(3)         s = abc.stringReturningMethod();
         }
         catch (SomeCheckedException e) {
            // somehow handle the exception
         }

         // do some other stuff
      }
   }
}
@Tested ClassUnderTest cut;

@Test
public void doSomethingHandlesSomeCheckedException(@Mocked DependencyAbc abc) throws Exception {
   new Expectations() {{
(1)   abc.intReturningMethod(); result = 3;

(2)   abc.stringReturningMethod();
      returns("str1", "str2");
      result = new SomeCheckedException();
   }};

   cut.doSomething();
}

灵活的参数匹配

注意:期望记录中的方法如果有具体参数,那么在运行时,和期望中的方法签名、参数以及参数具体的值一致的方法才会匹配期望(如果参数是对象,调用对象的equals来判别两个对象是否相同)。所以这是一种严格匹配。下面讲的是一种模糊匹配

“any”匹配(具体参数类型前面+any)
@Tested CodeUnderTest cut;

@Test
public void someTestMethod(@Mocked DependencyAbc abc) {
   DataItem item = new DataItem(...);

   new Expectations() {{
      // 匹配 "voidMethod(String, List)" 方法的调用
      abc.voidMethod(anyString, (List<?>) any);
   }};

   cut.doSomething(item);

   new Verifications() {{
      // Matches invocations to the specified method with any value of type long or Long.
      //匹配anotherVoidMethod(Long)
      abc.anotherVoidMethod(anyLong);
   }};
}
“with”匹配

withNotNull 不为null

withAny 任何的参数

withSubstring 任何包含某个字符串的字符串

@Test
public void someTestMethod(@Mocked DependencyAbc abc) {
   DataItem item = new DataItem(...);

   new Expectations() {{
      // Will match "voidMethod(String, List)" invocations with the first argument
      // equal to "str" and the second not null.
      abc.voidMethod("str", (List<?>) withNotNull());

      // Will match invocations to DependencyAbc#stringReturningMethod(DataItem, String)
      // with the first argument pointing to "item" and the second one containing "xyz".
      abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
   }};

   cut.doSomething(item);

   new Verifications() {{
      // Matches invocations to the specified method with any long-valued argument.
      abc.anotherVoidMethod(withAny(1L));
   }};
}

指定执行次数的限制

记录或者验证阶段都可以使用该限制。

如果是记录了限制,那么在实际运行时超过限制将会出错

@Tested CodeUnderTest cut;

@Test
public void someTestMethod(@Mocked DependencyAbc abc) {
   new Expectations() {{
      // By default, at least one invocation is expected, i.e. "minTimes = 1":
      new DependencyAbc();

      // At least two invocations are expected:
     //最少执行两次
      abc.voidMethod(); minTimes = 2;

      // 1 to 5 invocations are expected:
     //1-5次
      abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;
   }};

   cut.doSomething();
}

@Test
public void someOtherTestMethod(@Mocked DependencyAbc abc) {
   cut.doSomething();

   new Verifications() {{
      // Verifies that zero or one invocations occurred, with the specified argument value:
      abc.anotherVoidMethod(3); maxTimes = 1;

      // Verifies the occurrence of at least one invocation with the specified arguments:
      DependencyAbc.someStaticMethod("test", false); // "minTimes = 1" is implied
   }};
}

显示验证

在Verifications里面你可以使用类似于Expectations的语法来显示验证函数是否调用。注意可以加上”times=n”来限定调用的次数。

@Test
public void verifyInvocationsExplicitlyAtEndOfTest(@Mocked Dependency mock) {
   // Nothing recorded here, though it could be.

   // Inside tested code:
   Dependency dependency = new Dependency();
   dependency.doSomething(123, true, "abc-xyz");
   // 验证指定参数类型的doSomething至少被调用了一次
   // Verifies that Dependency#doSomething(int, boolean, String) was called at least once,
   // with arguments that obey the specified constraints:
   new Verifications() {{ mock.doSomething(anyInt, true, withPrefix("abc")); }};

验证函数调用顺序:

@Test
public void verifyingExpectationsInOrder(@Mocked DependencyAbc abc) {
   // Somewhere inside the tested code:
   abc.aMethod();
   abc.doSomething("blah", 123);
   abc.anotherMethod(5);
   ...
   //验证是否按照该顺序进行
   //doSomething的顺序没有验证,顺序以及是否产生调用都没有影响
   new VerificationsInOrder() {{
      // The order of these invocations must be the same as the order
      // of occurrence during replay of the matching invocations.
      abc.aMethod();
      abc.anotherMethod(anyInt);
   }};
}

Delegate:自定义结果(result, return)

适用于我们想根据函数调用的参数来决定函数运行的结果的场景

@Tested CodeUnderTest cut;

@Test
public void delegatingInvocationsToACustomDelegate(@Mocked DependencyAbc anyAbc) {
   new Expectations() {{
      anyAbc.intReturningMethod(anyInt, anyString);
      //使用Delegate来定制化结果
      result = new Delegate() {
         int aDelegateMethod(int i, String s) {
            return i == 1 ? i : s.length();
         }
      };
   }};

   // Calls to "intReturningMethod(int, String)" will execute the delegate method above.
   cut.doSomething();
}

注意:Delegate是个空接口,只是用来告诉JMockit哪个调用要被delegated,方法的名字没有限制,同时参数可以匹配真实的调用也可以没有,另外delegate方法有一个默认的参数Invocation,它可以获取运行时调用实例的相关参数。返回的参数也可以和记录的方法不相同,但正常情况下应该都相同,为了防止后面的类型转换错误。

构造函数也可以被delegate

@Test
public void delegatingConstructorInvocations(@Mocked Collaborator anyCollaboratorInstance) {
   new Expectations() {{
      new Collaborator(anyInt);
      result = new Delegate() {
         void delegate(int i) { if (i < 1) throw new IllegalArgumentException(); }
      };
   }};
   //第一个调用Collaborator(int)构造函数的实例将会执行上面的delegate方法
   // The first instantiation using "Collaborator(int)" will execute the delegate above.
   new Collaborator(4);
}

验证时获取调用参数

T withCapture() : 针对单词调用

T withCapture(List< T>) : 针对多次调用

List< T> withCapture(T) :针对构造函数(新创建的实例)

单次调用的参数获取:

@Test
public void capturingArgumentsFromSingleInvocation(@Mocked Collaborator mock) {
   // Inside tested code:
   ...
   new Collaborator().doSomething(0.5, new int[2], "test");

   // Back in test code:
   new Verifications() {{
      double d;
      String s;
      mock.doSomething(d = withCapture(), null, s = withCapture());

      assertTrue(d > 0.0);
      assertTrue(s.length() > 1);
   }};
}

withCpture()只能在verification块里使用,如果产生了多次调用,只会获取最后一次调用的信息

多次调用的参数获取:

withCapture(List)也可以使用在expectation中

@Test
public void capturingArgumentsFromMultipleInvocations(@Mocked Collaborator mock) {
   // Inside tested code:
   mock.doSomething(dataObject1);
   mock.doSomething(dataObject2);
   ...

   // Back in test code:
   new Verifications() {{
      List<DataObject> dataObjects = new ArrayList<>();
      mock.doSomething(withCapture(dataObjects));

      assertEquals(2, dataObjects.size());
      DataObject data1 = dataObjects.get(0);
      DataObject data2 = dataObjects.get(1);
      // Perform arbitrary assertions on data1 and data2.
   }};
}

获取构造函数的参数

@Test
public void capturingNewInstances(@Mocked Person mockedPerson) {
   // From the code under test:
   dao.create(new Person("Paul", 10));
   dao.create(new Person("Mary", 15));
   dao.create(new Person("Joe", 20));
   ...

   // Back in test code:
   new Verifications() {{
      // Captures the new instances created with a specific constructor.
      List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));

      // Now captures the instances of the same type passed to a method.
      List<Person> personsCreated = new ArrayList<>();
      dao.create(withCapture(personsCreated));

      // Finally, verifies both lists are the same.
      assertEquals(personsInstantiated, personsCreated);
   }};
}

级联Mock

针对这种obj1.getObj2(…).getYetAnotherObj().doSomething(…)级联调用的Mock。我们上面所讲的三个注解都可以实现这个功能

@Test
public void recordAndVerifyExpectationsOnCascadedMocks(
   //将会匹配运行中任何新建的Socket对象
   @Mocked Socket anySocket, // will match any new Socket object created during the test
   //将会匹配级联产生的SocketChannel对象
   @Mocked SocketChannel cascadedChannel // will match cascaded instances
) throws Exception {
   new Expectations() {{
      // Calls to Socket#getChannel() will automatically return a cascaded SocketChannel;
      // such an instance will be the same as the second mock parameter, allowing us to
      // use it for expectations that will match all cascaded channel instances:
      cascadedChannel.isConnected(); result = false;
   }};
   //当调用getChannel时返回的就是cascadeChannel对象
   // Inside production code:
   Socket sk = new Socket(); // mocked as "anySocket"
   SocketChannel ch = sk.getChannel(); // mocked as "cascadedChannel"

   if (!ch.isConnected()) {
      SocketAddress sa = new InetSocketAddress("remoteHost", 123);
      ch.connect(sa);
   }

   InetAddress adr1 = sk.getInetAddress();  // returns a newly created InetAddress instance
   InetAddress adr2 = sk.getLocalAddress(); // returns another new instance
   ...

   // Back in test code:
   new Verifications() {{ cascadedChannel.connect((SocketAddress) withNotNull()); }};
}

静态工厂方法的级联:

下面的方法不用关心FacesContext.getCurrentInstance(),因为jsf就是这个调用的返回值

@Test
public void postErrorMessageToUIForInvalidInputFields(@Mocked FacesContext jsf) {
   // Set up invalid inputs, somehow.

   // Code under test which validates input fields from a JSF page, adding
   // error messages to the JSF context in case of validation failures.
   FacesContext ctx = FacesContext.getCurrentInstance();

   if (some input is invalid) {
      ctx.addMessage(null, new FacesMessage("Input xyz is invalid: blah blah..."));
   }
   ...

   // Test code: verify appropriate error message was added to context.
   new Verifications() {{
      FacesMessage msg;
      jsf.addMessage(null, msg = withCapture());
      assertTrue(msg.getSummary().contains("blah blah"));
   }};
}

对方法返回的是自身对象的级联Mock:

@Test
public void createOSProcessToCopyTempFiles(@Mocked ProcessBuilder pb) throws Exception {
   // Code under test creates a new process to execute an OS-specific command.
   String cmdLine = "copy /Y *.txt D:\\TEMP";
   File wrkDir = new File("C:\\TEMP");
   Process copy = new ProcessBuilder().command(cmdLine).directory(wrkDir).inheritIO().start();
   int exit = copy.waitFor();
   ...
   //验证pb.command使用特定的参数调用了start方法
   // Verify the desired process was created with the correct command.
   new Verifications() {{ pb.command(withSubstring("copy")).start(); }};
}

指定Mock的对象实例

@Mocked注解并不是指定哪个对象要被Mock,它是一种指定类型的Mock。因此如果指定某个对象被Mock,这时候就需要使用@Injectable注解,它是单例的,指定某一个实例被Mock。

注意:@Injectable Mock的实例是指传进来的或者是类里面注入的实例,不是调用构造函数创建的实例(因为是单例的,新创建的实例属于另一个对象了),另外静态方法( 静态方法属于类不属于对象)以及构造函数是不能被Mock的。

使用Injectable的好处就是单个定制化Mock对象的行为

public final class ConcatenatingInputStream extends InputStream
{
   private final Queue<InputStream> sequentialInputs;
   private InputStream currentInput;

   public ConcatenatingInputStream(InputStream... sequentialInputs) {
      this.sequentialInputs = new LinkedList<InputStream>(Arrays.asList(sequentialInputs));
      currentInput = this.sequentialInputs.poll();
   }

   @Override
   public int read() throws IOException {
      if (currentInput == null) return -1;

      int nextByte = currentInput.read();

      if (nextByte >= 0) {
         return nextByte;
      }

      currentInput = sequentialInputs.poll();
      return read();
   }
}
@Test
public void concatenateInputStreams(
   //注入两个Mock对象
   @Injectable InputStream input1, @Injectable InputStream input2
) throws Exception {
   new Expectations() {{
      input1.read(); returns(1, 2, -1);
      input2.read(); returns(3, -1);
   }};

   InputStream concatenatedInput = new ConcatenatingInputStream(input1, input2);
   byte[] buf = new byte[3];
   concatenatedInput.read(buf);

   assertArrayEquals(new byte[] {1, 2, 3}, buf);
}

使用Mock注解来定制化Mock实例:

@Mocked和@Capturing注解其实也能指定对象,只要我们定义多个mock类属性或参数就行(有区分)

@Test
public void matchOnMockInstance(
   @Mocked Collaborator mock, @Mocked Collaborator otherInstance
) {
   new Expectations() {{ mock.getValue(); result = 12; }};

   // Exercise code under test with mocked instance passed from the test:
   int result = mock.getValue();
   assertEquals(12, result);

   // If another instance is created inside code under test...
   Collaborator another = new Collaborator();

   // ...we won't get the recorded result, but the default one:
   assertEquals(0, another.getValue());
}

使用指定构造器(包括指定参数)创建的实例:

也就是指定在测试过程中创建的实例(测试前为创建)

需要注意的是,Expectations中定义的使用指定构造器构造的对象并不是一对一的映射,可以是多对一的映射(也就是 不是只生效一次)

@Test
public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator anyCollaborator) {
   // Record different behaviors for each set of instances:
   new Expectations() {{
      //下面两个使用了制定了构造器
      // One set, instances created with "a value":
      Collaborator col1 = new Collaborator("a value");
      col1.doSomething(anyInt); result = 123;

      // Another set, instances created with "another value":
      Collaborator col2 = new Collaborator("another value");
      col2.doSomething(anyInt); result = new InvalidStateException();
   }};

   // Code under test:
   new Collaborator("a value").doSomething(5); // will return 123
   //下面的将会抛异常
   new Collaborator("another value").doSomething(0); // will throw the exception
   ...
}

另一种机制;

@Test
public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator col1, @Mocked Collaborator col2) {
   new Expectations() {{
      // Map separate sets of future instances to separate mock parameters:
      new Collaborator("a value"); result = col1;
      new Collaborator("another value"); result = col2;
      //可以统一创建,统一record
      // Record different behaviors for each set of instances:
      col1.doSomething(anyInt); result = 123;
      col2.doSomething(anyInt); result = new InvalidStateException();
   }};

   // Code under test:
   new Collaborator("a value").doSomething(5); // will return 123
   ...
   new Collaborator("another value").doSomething(0); // will throw the exception
   ...
}

部分Mocking

jmockit默认所有的方法和构造函数都是被mock的,包括它的父类(除了Object)也会被Mock.但有时候我们只想部分Mock,不被Mock的部分仍按正常的逻辑执行。

案例:

Expectations可以传一个或多个类和Class,如果是传一个Class,该class中所有的方法和构造函数都可以被mocked(包括父类中的),该类的所有实例都会被认为是mock的对象。如果传的是对象,则只有方法(不包括构造函数)能被mock,而且仅有那个传的实例才能被mock。

注意:下面的这些方式有引出了另一种指定Mock对象类型的方式,也就是不通过类属性输入以及作为方法参数传入的方式来Mock某个类型

public class PartialMockingTest
{
   static class Collaborator
   {
      final int value;

      Collaborator() { value = -1; }
      Collaborator(int value) { this.value = value; }

      int getValue() { return value; }
      final boolean simpleOperation(int a, String b, Date c) { return true; }
      static void doSomething(boolean b, String s) { throw new IllegalStateException(); }
   }

   @Test
   public void partiallyMockingAClassAndItsInstances() {
      Collaborator anyInstance = new Collaborator();
      //传需要Mock的类
      new Expectations(Collaborator.class) {{
         anyInstance.getValue(); result = 123;
      }};

      // Not mocked, as no constructor expectations were recorded:
      Collaborator c1 = new Collaborator();
      Collaborator c2 = new Collaborator(150);

      // Mocked, as a matching method expectation was recorded:
      assertEquals(123, c1.getValue());
      assertEquals(123, c2.getValue());

      // Not mocked:
      assertTrue(c1.simpleOperation(1, "b", null));
      assertEquals(45, new Collaborator(45).value);
   }

   @Test
   public void partiallyMockingASingleInstance() {
      Collaborator collaborator = new Collaborator(2);

      new Expectations(collaborator) {{
         collaborator.getValue(); result = 123;
         collaborator.simpleOperation(1, "", null); result = false;

         // Static methods can be dynamically mocked too.
         Collaborator.doSomething(anyBoolean, "test");
      }};

      // Mocked:
      assertEquals(123, collaborator.getValue());
      assertFalse(collaborator.simpleOperation(1, "", null));
      Collaborator.doSomething(true, "test");

      // Not mocked:
      assertEquals(2, collaborator.value);
      assertEquals(45, new Collaborator(45).getValue());
      assertEquals(-1, new Collaborator().getValue());
   }
}

当我们指定某个类或者某个类型被部分mock时,它仍能在验证阶段使用,即使没有record过

@Test
public void partiallyMockingAnObjectJustForVerifications() {
   Collaborator collaborator = new Collaborator(123);

   new Expectations(collaborator) {};

   // No expectations were recorded, so nothing will be mocked.
   int value = collaborator.getValue(); // value == 123
   collaborator.simpleOperation(45, "testing", new Date());
   ...

   // Unmocked methods can still be verified:
   new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }};
}

另外,还有一个最简单部分mock的方法,就是同时对mock的类属性实例使用@Tested和@Mocked注解

捕获实现类和实例

假设我们想测试的方式是基于某个接口的,也就是我们要mock该接口的实现类。但是有些实现类是通过匿名方式来创建的,比如下面的Service2。下面就是基于这种情况来讨论

public interface Service { int doSomething(); }
final class ServiceImpl implements Service { public int doSomething() { return 1; } }

public final class TestedUnit
{
   private final Service service1 = new ServiceImpl();
   private final Service service2 = new Service() { public int doSomething() { return 2; } };

   public int businessOperation() {
      return service1.doSomething() + service2.doSomething();
   }
}

mock不确定的实现类,使用@Capturing注解,它能mock所有的实现类

public final class UnitTest
{
   @Capturing Service anyService;

   @Test
   public void mockingImplementationClassesFromAGivenBaseType() {
      new Expectations() {{ anyService.doSomething(); returns(3, 4); }};

      int result = new TestedUnit().businessOperation();

      assertEquals(7, result);
   }
}

指定待创建类的行为:

maxInstances指定最大多少个实例被指定为和当前capture对象的行为一致。@Capturing会mock所有的实现类,不管有没有maxInstances注解,至于被mock对象的行为,可以通过maxInstances来控制

@Test
public void testWithDifferentBehaviorForFirstNewInstanceAndRemainingNewInstances(
   //Buffer是个接口
   @Capturing(maxInstances = 1) Buffer firstNewBuffer, @Capturing Buffer remainingNewBuffers
) {
   new Expectations() {{
      firstNewBuffer.position(); result = 10;
      remainingNewBuffers.position(); result = 20;
   }};

   // Code under test creates several buffers...
   ByteBuffer buffer1 = ByteBuffer.allocate(100);
   IntBuffer  buffer2 = IntBuffer.wrap(new int[] {1, 2, 3});
   CharBuffer buffer3 = CharBuffer.wrap("                ");

   // ... and eventually read their positions, getting 10 for
   // the first buffer created, and 20 for the remaining ones.
   assertEquals(10, buffer1.position());
   assertEquals(20, buffer2.position());
   assertEquals(20, buffer3.position());
}

全验证和其他的验证法

全验证:也就是验证的步骤中少了一环,就会验证失败

@Test
public void verifyAllInvocations(@Mocked Dependency mock) {
   // Code under test included here for easy reference:
   mock.setSomething(123);
   mock.setSomethingElse("anotherValue");
   mock.setSomething(45);
   mock.save();

   new FullVerifications() {{
      // Verifications here are unordered, so the following invocations could be in any order.
      mock.setSomething(anyInt); // verifies two actual invocations
      mock.setSomethingElse(anyString);
      mock.save(); // if this verification (or any other above) is removed the test will fail
   }};
}

验证某个调用没被调用:使用”times=0”

部分顺序的验证:

@Mocked DependencyAbc abc;
@Mocked AnotherDependency xyz;

@Test
public void verifyingTheOrderOfSomeExpectationsRelativeToAllOthers() {
   new CodeUnderTest().doSomething();

   new VerificationsInOrder() {{
      abc.methodThatNeedsToExecuteFirst();
      //unverifiedInvocations表示未排序的方法可以在这块执行
      unverifiedInvocations(); // Invocations not verified must come here...
      xyz.method1();
      abc.method2();
      unverifiedInvocations(); // ... and/or here.
      xyz.methodThatNeedsToExecuteLast();
   }};
}
@Test
public void verifyFirstAndLastCallsWithOthersInBetweenInAnyOrder() {
   // Invocations that occur while exercising the code under test:
   mock.prepare();
   mock.setSomethingElse("anotherValue");
   mock.setSomething(123);
   mock.notifyBeforeSave();
   mock.save();
   //需要排序的验证和不需要排序的验证分成两个部分来写
   new VerificationsInOrder() {{
      mock.prepare(); // first expected call
      unverifiedInvocations(); // others at this point
      mock.notifyBeforeSave(); // just before last
      mock.save(); times = 1; // last expected call
   }};

   // Unordered verification of the invocations previously left unverified.
   // Could be ordered, but then it would be simpler to just include these invocations
   // in the previous block, at the place where "unverifiedInvocations()" is called.
   new Verifications() {{
      mock.setSomething(123);
      mock.setSomethingElse(anyString);
   }};
}

排序的全验证:

@Test
public void verifyAllInvocationsInOrder(@Mocked Dependency mock) {
   // Code under test included here for easy reference:
   mock.setSomething(123);
   mock.setSomethingElse("anotherValue");
   mock.setSomething(45);
   mock.save();

   new FullVerificationsInOrder() {{
      mock.setSomething(anyInt);
      mock.setSomethingElse(anyString);
      mock.setSomething(anyInt);
      mock.save();
   }};
}

针对某Mock对象的全验证:

@Test
public void verifyAllInvocationsToOnlyOneOfTwoMockedTypes(
   @Mocked Dependency mock1, @Mocked AnotherDependency mock2
) {
   // Inside code under test:
   mock1.prepare();
   mock1.setSomething(123);
   mock2.doSomething();
   mock1.editABunchMoreStuff();
   mock1.save();

   new FullVerifications(mock1) {{
      mock1.prepare();
      mock1.setSomething(anyInt);
      mock1.editABunchMoreStuff();
      mock1.save(); times = 1;
   }};
}

验证没有调用发生:

注意下面的代码:为什么doSomething不在验证阶段统计?因为Expectations有doSomthing的期望,默认是被验证的,所以在Verifications里面被忽略。同时如果前面的验证也验证了某些方法,后面的全验证也会省略

@Test
public void verifyNoInvocationsOnOneOfTwoMockedDependenciesBeyondThoseRecordedAsExpected(
   @Mocked Dependency mock1, @Mocked AnotherDependency mock2
) {
   new Expectations() {{
      // These two are recorded as expected:
      mock1.setSomething(anyInt);
      mock2.doSomething(); times = 1;
   }};

   // Inside code under test:
   mock1.prepare();
   mock1.setSomething(1);
   mock1.setSomething(2);
   mock1.save();
   mock2.doSomething();

   // Will verify that no invocations other than to "doSomething()" occurred on mock2:
   new FullVerifications(mock2) {};
}

验证未指定的调用不应该发生:

@Test
public void readOnlyOperation(@Mocked Dependency mock) {
   new Expectations() {{ mock.getData(); result = "test data"; }};

   // Code under test:
   String data = mock.getData();
   // mock.save() should not be called here
   ...

   new FullVerifications() {{
      //只有getData能够被调用,其他的方法如果被调用就会失败,minTimes应该是个标记
      mock.getData(); minTimes = 0; // calls to getData() are allowed, others are not
   }};
}

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;