概述

在这篇文章中,我们将深入探讨JMockit的高级用法,涉及的内容包括:

  • 模拟(或MockUp API)
  • 如何仅使用一个模拟对象mock多个接口
  • 如何重用期望和验证

对于JMockit的基础知识,您可以参考系列文章中的其他内容,底部会提供相关链接。

2. Maven依赖

首先,我们需要在项目中添加JMockit的依赖:

<dependency> 
    <groupId>org.jmockit</groupId> 
    <artifactId>jmockit</artifactId> 
    <version>1.49</version>
</dependency>

接下来,我们继续看示例。

3. 保护方法的模拟

有时,我们需要模拟受保护的方法。在JMockit中,我们可以使用MockUp API来修改受保护方法的真正实现。

所有示例都将针对以下类进行演示,并假设我们在测试类中使用与第一个相同的配置(以避免重复代码):

public class AdvancedCollaborator {
    int i;
    private int privateField = 5;

    // default constructor omitted 
    
    public AdvancedCollaborator(String string) throws Exception{
        i = string.length();
    }

    public String methodThatCallsProtectedMethod(int i) {
        return protectedMethod() + i;
    }
    public int methodThatReturnsThePrivateField() {
        return privateField;
    }
    private String protectedMethod() {
        return "default:";
    }

    class InnerAdvancedCollaborator {...}
}

JMockit的MockUp API提供了创建模拟实现(或称为“mock-up”)的支持。通常,mock-up只针对要模拟的类中的几个方法和/或构造函数,而保留其他大部分方法和构造函数不变。JMockit支持模拟受保护的方法。

让我们看看如何使用MockUp API重新定义protectedMethod()

public class AdvancedCollaboratorIntegrationTest {

    @Tested
    private AdvancedCollaborator mock;

    @Test
    public void testToMockUpProtectedMethod() {
        new MockUp<AdvancedCollaborator>() {
            @Mock
            private String protectedMethod() {
                return "mocked: ";
            }
        };
        String res = mock.methodThatCallsProtectedMethod(1);
        assertEquals("mocked: 1", res);
    }
}

在这个例子中,我们在匹配签名的方法上使用了@Mock注解来定义一个新的AdvancedCollaborator类的mock-up。此后,对该方法的调用将被委托给我们的模拟版本。

我们还可以使用它来简化测试,模拟需要特定参数或配置的类的构造函数:

@Test
public void testToMockUpDifficultConstructor() throws Exception{
    new MockUp<AdvancedCollaborator>() {
        @Mock
        public void $init(Invocation invocation, String string) {
            ((AdvancedCollaborator)invocation.getInvokedInstance()).i = 1;
        }
    };
    AdvancedCollaborator coll = new AdvancedCollaborator(null);
    assertEquals(1, coll.i);
}

在这个示例中,可以看到为了模拟构造函数,我们需要模拟$init方法。我们可以传递一个类型为Invocation的额外参数,以便访问关于模拟方法调用的信息,包括调用所作用的对象。

4. 私有字段的模拟

通常不建议直接测试私有字段,因为它们是类的内部细节。但在处理遗留代码时,有时仍然需要这样做。

在JMockit中,我们可以使用@Injectable注解来模拟我们的私有字段。

设置私有字段:

@Test
public void testToSetPrivateFieldDirectly(@Injectable("10") int privateField){
    assertEquals(10, privateField);
}

获取字段:

@Test
public void testToGetPrivateFieldDirectly(){
    assertEquals(5, mock.methodThatReturnsThePrivateField());
}

5. 一次模拟多个接口

假设我们要测试一个尚未实现但肯定将实现多个接口的类。

通常,在实现之前无法测试此类,但使用JMockit,我们可以在实现之前提前准备测试,通过一个模拟对象mock多个接口。

这可以通过使用泛型并定义一个扩展多个接口的类型来实现。这个泛型类型可以应用于整个测试类,也可以应用于单个测试方法。

例如,我们将为ListComparator接口创建一个模拟:

public class AdvancedCollaboratorIntegrationTest
    
    interface IList<T> extends List<T> {}
    interface IComparator extends Comparator<Integer>, Serializable {}
    static class MultiMock {
        IList<?> get() { return null; }
        IComparator compareTo() { return null; }
    } 
    
    @Test
    public void testMultipleInterfacesWholeTest(@Mocked MultiMock multiMock) {
        new Expectations() {
            {
                multiMock.get(); result = null;
                multiMock.compareTo(); result = null;
            }
        };
        assertNull(multiMock.get());
        assertNull(multiMock.compareTo());
    } 
}

如图所示,我们在测试方法上定义了一个新的静态MultiMock类。这样,MultiMock就会作为类型存在,我们可以使用JMockit的@Mocked注解创建它的模拟。

如果需要在方法中使用多接口模拟,可以在方法签名上定义@Mocked注解,并将模拟作为测试方法的参数传递。

6. 重用期望和验证

在测试类的过程中,可能会遇到反复使用期望和验证的情况。为了简化这一点,我们可以轻松地重用它们。

我们将通过一个例子来解释(使用我们在JMockit 101文章中提到的ModelCollaboratorPerformer类):

public class ReusingIntegrationTest {

    @Injectable
    private Collaborator collaborator;
    
    @Mocked
    private Model model;

    @Tested
    private Performer performer;
    
    @Before
    public void setup(){
        new Expectations(){{
           model.getInfo(); result = "foo"; minTimes = 0;
           collaborator.collaborate("foo"); result = true; minTimes = 0; 
        }};
    }

    @Test
    public void testWithSetup() {
        performer.perform(model);
        verifyTrueCalls(1);
    }
    
    protected void verifyTrueCalls(int calls){
        new Verifications(){{
           collaborator.receive(true); times = calls; 
        }};
    }
    
    final class TrueCallsVerification extends Verifications{
        public TrueCallsVerification(int calls){
            collaborator.receive(true); times = calls; 
        }
    }
    
    @Test
    public void testWithFinalClass() {
        performer.perform(model);
        new TrueCallsVerification(1);
    }
}

在这个例子中,我们在setup()方法中为每个测试准备了期望,确保model.getInfo()始终返回"foo",而collaborator.collaborate()总是期待"foo"作为参数并返回true。我们使用minTimes = 0语句是为了在测试中不实际使用它们时不会出现失败。

此外,我们还创建了verifyTrueCalls(int)方法,以简化对collaborator.receive(boolean)方法的验证,当传递的参数为true时。

最后,我们还可以通过扩展任何期望或验证类创建特定类型的期望和验证。然后,如果需要配置行为,我们可以定义构造函数,并在测试中创建此类的新实例。

7. 总结

通过本篇JMockit系列文章,我们探讨了一些高级主题,这些无疑将帮助我们日常的模拟和测试工作。

我们可能还会有关于JMockit的更多文章,请关注以学习更多内容。

一如既往,本教程的完整实现可在GitHub上找到。

7.1. 系列文章

系列文章的所有内容: