2 Mocking

JMockit提供了两套API,一套叫做Expectations,用于基于行为的单元测试;一套叫做Faking,用于基于状态的单元测试。

基于Expectations的单元测试创建mock对象,并且record相应的行为,之后调用被测代码(CodeUnderTest),然后进行verify。

Mocking主要考察被测试类与其依赖之间是否正确交互,交互的形式为方法调用。也就是说,Mocking考察被测试类是否正确调用了其依赖,正确调用包括:

  • 调用了那些方法
  • 通过怎样的参数
  • 调用了多少次
  • 调用的相对顺序

关于Mocking和Faking,有两篇文章对其进行了区分:
Mocks Aren't Stubs
Stub, Mock and Proxy Testing

2.1 创建并使用mock对象

JMockit可以mock任意class、interface。可以将mock对象声明为域或者方法的参数。默认情况下,mock对象的所有 非private 的方法(包括除了object的其他继承方法)都会被mock,对这些方法的调用不会执行原有代码,而是会转交给JMockit处理。进行mock风格的测试需要三个步骤:expectation--> 方法调用 --> verication,示例如下:

@Mocked Dependency mockedDependency

@Test
public void test(final @Mocked AnotherDenpendency anotherDependency) {
    new Expectations() {{
        mockedDependency.mockedMethod();
        result = 1;
    }};

    codeUnderTest();

    new Verifications(){{
        anotherDependency.anotherMethod();
        times = 1;
    }};
}

JMockit会对@Mocked注解的对象进行依赖注入,所以在Expectation、Verication以及CodeUnderTest中可以直接使用mock对象,不需要手动实例化。

在CodeUnderTest中通过new创建了一个Dependency并调用了其方法,JMockit会自动将这个方法调用转移到mock对象上。

public class CodeUnderTest {
    public int testMethod() {
        Dependency dependency = new Dependency();
        return dependency.mockMethod();
    }
}

public class Dependency {
    public int mockMethod() {
        return 1;
    }
}

Dependency类的mockMethod方法原本返回值为1,在Expectation中将其返回值设置为2,则在测试过程中该方法将会返回2。

@Mocked
Dependency dependency;

@Test
public void TestMethod() throws Exception {
    new NonStrictExpectations() {{
        dependency.mockMethod();
        result = 2;
    }};

    CodeUnderTest codeUnderTest = new CodeUnderTest();
    assertEquals(2, codeUnderTest.testMethod());
}

2.2 Expectations

Expectations中定义了mock对象将会被调用的方法以及方法的返回值。Expectations中出现的方法必须被调用,而调用的方法不必全部出现在Expectation中。

但是,如果定义了mock对象,并在测试代码中调用了它的某个方法,而该方法没有出现在Expectation中,JMockit并不会执行其原有代码,而是返回null或者原始类型的初始值。例如:

public class CodeUnderTest {
    public int testMethod() {
        Dependency dependency = new Dependency();
        return dependency.mockMethod();
    }
}
public class Dependency {
    public int mockMethod() {
        return 1;
    }
}
@RunWith(JMockit.class)
public class MyTest {

    @Mocked
    Dependency dependency;

    @Test
    public void TestMethod() throws Exception {
        CodeUnderTest codeUnderTest = new CodeUnderTest();
        assertEquals(0, codeUnderTest.testMethod());
    }
}

2.3 record-replay-verify模型

record : 录制将要被调用的方法和返回值
replay:调用录制的方法
verify:基于行为的验证

在record阶段实例化Expectations, 在verify阶段实例化Verifications。一个测试方法可以包括任意个(包括0)Expectation/Verification。

@Test
public void testMethod(Parameter p) {
    //常规准备代码
    
    //record
    new Expectations(){};
    
    //replay
    //调用测试代码
    
    //verify
    new Verifications(){};
    
    //其他验证代码
}

2.4 Regular v.s. Strict Expectation

NonStrictExpections中的方法至少被调用一次,否则会出现missing invocation错误。之所以说它是常规的,是因为其中的方法可以调用多次,也可以颠倒顺序,其中没出现的方法也可以调用。

StrictExpectations中方法调用的次数顺序都必须严格执行。同时,如果出现了在StrictExpectations中没有声明的方法,会出现unexpected invocation错误。

  • 可以混合使用StrictExpectationsNonStrictExpections,不过一般一个mock对象只出现在其中之一。
  • StrictExpectations包含了隐式的verification。

2.5 为Expectation录制结果

对于返回值非空的函数(包括构造器),可以通过result设置返回值抛出异常,该值在replay阶段生效。

可以record多个结果,

mockObject.mockMethod();
result = new Object();
result = new Object();
result = new SomeException();

等价于

mockObject.mockMethod();
returns(new Obejct(), new Object());
result = new SomeException();

Note:

  • 返回值可以使用returns()函数,异常值必须要使用result
  • 异常值需要在CodeUnderTest中捕获,否则无法通过测试。
  • 假设在StrictExpectations中录制了n个结果,在replay阶段并不强制要求调用n次该函数,调用1次即可。

2.6 调用特定对象的方法

通常,声明了mock对象并在Expectation中进行了record,则在replay阶段对该类其他对象的调用也会返回record的结果。也就是说,在replay阶段,JMockit并不关心调用的是哪个对象,只要是该mock类的对象就会引用record中的结果。

示例如下:

@Test
public void TestMethod(@Mocked final Dependency dependency) throws Exception {
    new NonStrictExpectations() {{
        dependency.intReturnMethod();
        returns(1, 2, 3);
    }};

    Dependency dependency1 = new Dependency();
    assertEquals(1, dependency1.intReturnMethod());

    Dependency dependency2 = new Dependency();
    assertEquals(2, dependency2.intReturnMethod());

    Dependency dependency3 = new Dependency();
    assertEquals(3, dependency3.intReturnMethod());
}

在大多数情况下,CodeUnderTest使用mock类的某一个对象,所以是在CodeUnderTest中创建的还是作为参数传给它的并不重要。但是,如果CodeUnderTest中包含多个mock对象,而我们需要

  • 只mock其中某个对象,其它的并不mock
  • 指定调用某个mock对象的方法

这时,使用 @Injectable 可以mock某个对象。当然,即使用 @Mock 注解mock该类所有对象,也有其他方法限制Expectation中的匹配。

@Injectable

public class Dependency {
    public String mockMethod() {
        return "realMethod";
    }
}
@Test
public void TestMethod(@Injectable final Dependency mockDependency) {
    new NonStrictExpectations() {{
        mockDependency.mockMethod();
        result = "mockMethod";
    }};
    assertEquals("mockMethod", mockDependency.mockMethod());
    assertEquals("realMethod", new Dependency().mockMethod());
}

其他对象不受影响,正常执行原有代码。

note:

  • 需要将mock对象传递给CodeUnderTest
  • static方法和constructor无法被mock

声明多个mock对象

声明多个mock对象可以限制和Expectation的匹配。

示例如下:

//参数中的anotherDependency只是起到占位作用,不会被真正使用
@Test
public void TestMethod(@Mocked final Dependency mockDependency, 
                       @Mocked Dependency anotherDependency) {
    new NonStrictExpectations() {{
        mockDependency.mockMethod();
        result = "mockMethod";
    }};
    
    //这个会被mock
    assertEquals("mockMethod", mockDependency.mockMethod());
    
    //JMockit仍然拦截了这次调用,但是由于在Expectation中没有record,所以返回null
    assertNull("realMethod", new Dependency().mockMethod());
}

这种方法看起来比较奇怪,主要用于当CodeUnderTest中包含多个Dependency类的对象,而想要测试其中某个确定对象会被调用。

mock特定constructor产生的实例

有两种方式可以实现这个效果,方式1:

@Test
//mockDependency不会被使用
public void TestMethod(@Mocked Dependency mockDependency) {
    new NonStrictExpectations() {{
        Dependency dependency1 = new Dependency("dependency1");
        dependency1.mockMethod(); result="dependency1";
        Dependency dependency2 = new Dependency("dependency2");
        dependency2.mockMethod(); result="dependency2";
    }};
    
    //可以创建多个对象,会匹配到同一个Expectation
    assertEquals("dependency1", new Dependency("dependency1").mockMethod());
    assertEquals("dependency1", new Dependency("dependency1").mockMethod());
    
    assertEquals("dependency2", new Dependency("dependency2").mockMethod());
    
    //JMockit仍然拦截了这次调用,但是由于在Expectation中没有record,所以返回null
    assertNull(new Dependency("dependency2").mockMethod());
}

方式2:

@Test
public void TestMethod(@Mocked final Dependency mockDependency1,
                       @Mocked final Dependency mockDependency2) {
    new NonStrictExpectations() {{
        new Dependency("dependency1");
        result = mockDependency1;
        new Dependency("dependency2");
        result = mockDependency2;
        mockDependency1.mockMethod();
        result = "dependency1";
        mockDependency2.mockMethod();
        result = "dependency2";
    }};
    assertEquals("dependency1", new Dependency("dependency1").mockMethod());
    assertEquals("dependency1", new Dependency("dependency1").mockMethod());
    assertEquals("dependency2", new Dependency("dependency2").mockMethod());
    assertNull("dependency3", new Dependency("dependency3").mockMethod());
}

两种方法等效。

2.7 灵活的参数匹配

在record和verify阶段进行方法匹配时,

  • 对于原始类型对象,数值相同即可;
  • 对于Object的子类,需要equals()返回true;
  • 对于数组,需要长度相等且每个对象equals()返回true;

除此之外,如果不关心replay时的具体参数,可以使用anyXyz或者withXyz(...)方法。

使用"any"

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

   new Expectations() {{
      abc.voidMethod(anyString, (List) any);
   }};

   new UnitUnderTest().doSomething(item);

   new Verifications() {{
      abc.anotherVoidMethod(anyLong);
   }};
}
  • 任何的基本类型都有对应的anyXyzanyString对应任意字符串。
  • any对应任意的对象,在使用时需要进行显式类型转换: (CastClass) any
  • mockit.Invocations类中有可以使用所有anyXyz
  • 使用时参数位置需要一致

使用"with"

any的限制太宽松,with可以选择特定的子集。

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

   new Expectations() {{
      abc.voidMethod("str", (List) withNotNull());
      abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
   }};

   new UnitUnderTest().doSomething(item);

   new Verifications() {{
      abc.anotherVoidMethod(withAny(1L));
   }};
}

也可以自定义with方法。

使用"null"

null可以与任何对象匹配,好处是避免类型转换,但是需要有一个any或者with

@Test
public void TestMethod(@Mocked final Dependency mock) {
    new StrictExpectations() {{
    
        //测试会失败,因为没有any或者with
        mock.mockMethod(2,  null);
        
        //测试通过
        mock.mockMethod(anyInt,  null);
    }};
    mock.mockMethod(new Integer(2), "hello world");
}

如何需要的是null,则应该用 withNull() 方法。

varargs

要么使用常规的参数,要么使用any/with,不能混合使用。

2.8 调用次数的限制

在record和verify阶段可以使用timesminTimesmaxTimes来限制。
默认为minTimes = 1

2.9 显式验证

对于NonStrictExpectation,可以进行verification。对于StrictExpectation则没有必要。在new Verifications(){}中的方法至少被调用一次。

验证某个方法没被调用

times = 0

验证顺序调用

普通的new Verifications(){}没有验证其中方法的调用顺序。new VerificationsInOrder(){}用来验证(相对)顺序。

验证部分顺序

使用unverifiedInvocations()方法固定不需要验证的方法的位置。

第一种场景是验证部分方法的顺序,其余方法不需要验证:

@Test
public void TestMethod(@Mocked final Dependency mock) {
    mock.mockMethod1();
    mock.mockMethod2();
    mock.mockMethod3();
    mock.mockMethod4();

    new VerificationsInOrder(){
        {
            // 下面的代码会失败:
            // Unexpected invocation of: Dependency#mockMethod2()
            // 如果两个方法相连,则其在replay中也必须直接相连
            // unverifiedInvocations();
            // mock.mockMethod1();
            // mock.mockMethod4();
            
            // 成功
            mock.mockMethod1();
            unverifiedInvocations();
            mock.mockMethod4();
        }
    };
}

第二种场景是关心部分方法顺序,另一些方法也需要验证,但是不关心顺序。这时需要两个Verification块:

@Test
public void TestMethod(@Mocked final Dependency mock) {
    mock.mockMethod1();
    mock.mockMethod2();
    mock.mockMethod3();
    mock.mockMethod4();

    new VerificationsInOrder(){{
            mock.mockMethod1();
            unverifiedInvocations();
            mock.mockMethod4();
    }};

    new Verifications(){{
        mock.mockMethod3();
        mock.mockMethod2();
    }};
}

多个verification块时,其相对顺序会引起比较诡异的事:

@Test
public void TestMethod(@Mocked final Dependency mock) {
    mock.mockMethod1();
    mock.mockMethod2();
    mock.mockMethod3();
    mock.mockMethod4();
    
    //下面的代码会失败
    //MissingInvocation: Missing invocation of:Dependency#mockMethod2()
    //颠倒一下两个verification的顺序则会通过
    //原因似乎是Verifications会将验证过的方法删除

    new Verifications(){{
        mock.mockMethod3();
        mock.mockMethod2();
    }};

    new VerificationsInOrder(){{
        mock.mockMethod1();
        mock.mockMethod2();
        mock.mockMethod4();
    }};
}

full verification

new FullVerifications() {...}可以保证replay阶段调用的所有方法在verify代码块中都有相应的匹配,顺序可以不一致。

full verification in order

使用new FullVerificationsInOrder()

限制full verification的目标类型

默认使用full verification时,所有mock类的所有调用都必须显式验证。如果需要限定验证的类或者实例,使用FullVerifications(xxx.class)或者FullVerifications(mockObject)

验证没有调用发生

使用空的FullVerifications(xxx.class)或者FullVerifications(mockObject)可以验证在指定类/实例上没有调用方法。但是如果Expectation中有minTimes和times的方法会被正常验证。

2.10 在verification中捕获调用参数

单次调用捕获

使用withCapture()捕获最后一次调用的参数。

@Test
public void capturingArgumentsFromSingleInvocation(@Mocked final Collaborator mock)
{
   new Collaborator().doSomething(0.5, new int[2], "test");

   new Verifications() {{
      double d;
      String s;
      mock.doSomething(d = withCapture(), null, s = withCapture());

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

多次调用捕获

使用withCapture(List)捕获所有参数。

@Test
public void capturingArgumentsFromMultipleInvocations(@Mocked final Collaborator mock)
{
   mock.doSomething(dataObject1);
   mock.doSomething(dataObject2);

   new Verifications() {{
      List 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.
   }};
}

捕获新实例

使用withCapture(new XX())

@Test
public void capturingNewInstances(@Mocked Person mockedPerson) {
   new Person("Paul", 10);
   new Person("Mary", 15);
   new Person("Joe", 20);

   new Verifications() {{
      List personsInstantiated = withCapture(new Person(anyString, anyInt));
   }};
}

2.11 使用Delegate在Expectation中定制result

使用场景:在Expectation中需要根据replay时的参数值决定返回值。
原理:JMockit拦截调用,转交给Delegate处理。

@Test
public void delegatingInvocationsToACustomDelegate(@Mocked final DependencyAbc anyAbc){
   new Expectations() {{
      anyAbc.intReturningMethod(anyInt, null);
      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.
   new UnitUnderTest().doSomething();
}
  • delegate方法的参数应该与原始方法一致,返回值需要兼容或者为异常。
  • 可以delegate构造器,这时返回值设置为空。
  • delegate参数中可以有一个 Invocation对象,从而获得调用者的引用。

2.12 级联mock

出现obj1.getObj2(...).getYetAnotherObj().doSomething(...)时可能需要mock多个对象。对于一个mock对象:

  • Expectation中进行了record,则会返回record的result;
  • 如果没有record,JMockit会自动创建一个返回被注解@Injectable的子对象
public class Dependency {

    public CascadeDependency getCascadeDependency() {
        //JMockit会拦截这个方法,返回一个非null对象
        return null;
    }
    
    public CascadeDependency getAnotherCascadeDependency() {
        //JMockit会拦截这个方法,返回一个非null对象
        return null;
    }

    public String getString() {
        //仍旧返回null
        return null;
    }

    public Object getObject() {
        //仍旧返回null
        return null;
    }

    public List getList() {
        //返回empty集合
        return null;
    }
}

@Test
public void TestMethod(@Mocked Dependency dependency) {

    CascadeDependency first = dependency.getCascadeDependency();
    CascadeDependency second = dependency.getCascadeDependency();
    
    //调用另一个方法
    CascadeDependency third = dependency.getAnotherCascadeDependency();
    
    //所有都不会为null
    assertNotNull(first);
    assertNotNull(second);
    assertNotNull(third);
    
    //相同方法返回JMockit创建的同一个对象
    assertSame(first, second);
    
    //不同方法返回JMockit创建的同一个对象
    assertNotSame(first, third);
    
    //String返回null
    assertNull(dependency.getString());

    //Object返回null
    assertNull(dependency.getObject());
    
    //返回empty集合
    assertNotNull(dependency.getList());
    assertEquals(0, dependency.getList().size());
}
@Test
public void TestMethod(@Mocked Dependency dependency, 
                       @Mocked CascadeDependency cascadeDependency) {

    CascadeDependency first = dependency.getCascadeDependency();
    CascadeDependency second = dependency.getAnotherCascadeDependency();
    
    //因为子对象也@Mocked,所以会返回同一个对象
    assertSame(first, second);
}

JMockit返回的非空对象实际上进行了@Injectable标识,所以:

@Test
public void TestMethod(@Mocked Dependency dependency) {

    //虽然CascadeDependency没有出现在参数中,
    //但是JMockit对其进行了@Injectable
    //而由于没有在Expectation中record mockMethod的result,所以返回空
    assertNull(dependency.getCascadeDependency().mockMethod());

    //不影响CascadeDependency的其他实例
    assertNotNull(new CascadeDependency().mockMethod());
}

也可以在Expectation中使用result指定返回对象,从而禁止JMockit自动生成。

@Test
public void TestMethod(@Mocked final Dependency dependency) {

    //在Expectation中指定了返回结果,因此JMockit不会生成CascadeDependency
    new NonStrictExpectations(){{
        dependency.getCascadeDependency();
        result = null;
        result = new CascadeDependency();
    }};
    
    //第一次返回null
    assertNull(dependency.getCascadeDependency());
    
    //第二次返回新对象
    assertNotNull(dependency.getCascadeDependency().mockMethod());
}

mock级联调用特别适合static factory,getCurrentInstance()永远不会返回null。

@Test
public void TestMethod(@Mocked final Dependency dependency) {
    assertSame(dependency, dependency.getCurrentInstance());
}

在Builder模式中也很方便验证,

@Test
public void createOSProcessToCopyTempFiles(@Mocked final ProcessBuilder pb) throws Exception{
    Process copy = new ProcessBuilder().command(cmdLine).directory(wrkDir).inheritIO().start();

   new Verifications() {{ pb.command(withSubstring("copy")).start(); }};
}

2.13 部分mock

有时候只需要mock部分方法,这时候可以用new Expectations(object),object可以是实例,也可以是class对象。在replay阶段,如果在Expectation中没有进行record,则会调用原有代码。

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

  new Expectations(collaborator) {{
     collaborator.getValue(); result = 123;

     // 静态方法也可以
     Collaborator.doSomething(anyBoolean, "test");
  }};

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

  // Not mocked:
  assertEquals(45, new Collaborator(45).getValue());
}
  • Note:上面的代码中没有出现@Mocked注解

没有record的方法也可以verify,

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

  new Expectations(collaborator) {};
  
  int value = collaborator.getValue(); 
  collaborator.simpleOperation(45, "testing", new Date());

  // 没有record也可以verify
  new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }};
}

另一种实现部分mock的方法:同时标注@Tested和@Mocked。

2.14 mock接口

有些实现类是匿名的:

public interface Service { int doSomething(); }

public final class TestedUnit {

    private final Service service = new Service() { 
        public int doSomething() { return 2; } 
    };

    public int businessOperation() {
        return service.doSomething();
    }
}

使用@Capturing标注基类/接口,所有实现类会被mock:

@Capturing Service anyService;

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

    int result = new TestedUnit().businessOperation();
    assertEquals(3, result);
}

@Capturing@Mock的增强版,有一个可选参数maxInstances用于捕获前面指定数量的对象,其默认值为Integer.MAX_VALUE

@Test
public void TestMethod(@Capturing(maxInstances = 2) final Dependency dependency1,
                       @Capturing(maxInstances = 2) final Dependency dependency2,
                       @Capturing final Dependency remain) {
    new NonStrictExpectations() {{
        dependency1.getValue();
        result = 1;
        dependency2.getValue();
        result = 2;
        remain.getValue();
        result = 3;
    }};
    assertEquals(1, new Dependency().getValue());
    assertEquals(1, new Dependency().getValue());
    assertEquals(2, new Dependency().getValue());
    assertEquals(2, new Dependency().getValue());
    assertEquals(3, new Dependency().getValue());
}

上面的@Capturing是出现在参数列表中的,如果是作为field声明的,maxInstances会失效,@Capturing退化为@Mock

2.15 自动注入被测试类

@Tested标注被测试类,在运行测试方法时,如果该实例仍然为null,JMockit会自动组装相关mock对象,进行初始化。在组装被测试类过程中,相关mock对象必须使用@Injectable标记,非mock对象除了使用@Injectable标记,还需要有明确初始值。

public class SomeTest {
    @Tested CodeUnderTest tested;
    @Injectable Dependency dep1;
    @Injectable AnotherDependency dep2;
    @Injectable int someIntegralProperty = 123;

    @Test
    public void someTestMethod(@Injectable("true") boolean flag) {
        tested.exerciseCodeUnderTest();
    }
}

注入先根据类型匹配,再根据参数名称匹配。

你可能感兴趣的:(2 Mocking)