1. 概述

本文是JMockit系列文章的第二篇。建议先阅读第一篇基础教程,因为本文假设你已经掌握了JMockit的基本用法。

今天我们将深入探讨Expectations的核心机制,重点展示如何定义更精确或更通用的参数匹配,以及更高级的返回值定义方式。

2. 参数值匹配

以下方法同时适用于ExpectationsVerifications

2.1. "Any" 字段

JMockit提供了一组实用字段用于实现更通用的参数匹配,其中最常用的是anyX系列字段

这些字段会验证是否传入了任意值,每种基本类型(及其包装类)都有对应字段,字符串有专用字段,还有一个通用的Object类型字段。

看个例子:

public interface ExpectationsCollaborator {
    String methodForAny1(String s, int i, Boolean b);
    void methodForAny2(Long l, List<String> lst);
}

@Test
public void test(@Mocked ExpectationsCollaborator mock) throws Exception {
    new Expectations() {{
        mock.methodForAny1(anyString, anyInt, anyBoolean); 
        result = "any";
    }};

    Assert.assertEquals("any", mock.methodForAny1("barfooxyz", 0, Boolean.FALSE));
    mock.methodForAny2(2L, new ArrayList<>());

    new FullVerifications() {{
        mock.methodForAny2(anyLong, (List<String>) any);
    }};
}

注意:使用any字段时需要强制转换为预期类型。完整字段列表见官方文档

2.2. "With" 方法

JMockit还提供了多个withX方法实现更灵活的参数匹配

相比anyX字段,这些方法支持更复杂的匹配逻辑。下面这个例子中,我们定义了以下匹配规则:包含"foo"的字符串、不等于1的整数、非null的Boolean值,以及任意List实例:

public interface ExpectationsCollaborator {
    String methodForWith1(String s, int i);
    void methodForWith2(Boolean b, List<String> l);
}

@Test
public void testForWith(@Mocked ExpectationsCollaborator mock) throws Exception {
    new Expectations() {{
        mock.methodForWith1(withSubstring("foo"), withNotEqual(1));
        result = "with";
    }};

    assertEquals("with", mock.methodForWith1("barfooxyz", 2));
    mock.methodForWith2(Boolean.TRUE, new ArrayList<>());

    new Verifications() {{
        mock.methodForWith2(withNotNull(), withInstanceOf(List.class));
    }};
}

完整的withX方法列表见官方文档。注意:特殊的*with(Delegate)*方法将在单独小节讲解。

2.3. Null的特殊含义

需要特别注意:null在参数匹配中并不表示实际传入null值,而是语法糖,表示"任意对象"(仅适用于引用类型参数)。要验证实际传入null,需使用*withNull()*匹配器。

下面这个例子中,我们定义的行为触发条件是:任意字符串、任意List、以及null引用:

public interface ExpectationsCollaborator {
    String methodForNulls1(String s, List<String> l);
    void methodForNulls2(String s, List<String> l);
}

@Test
public void testWithNulls(@Mocked ExpectationsCollaborator mock){
    new Expectations() {{
        mock.methodForNulls1(anyString, null); 
        result = "null";
    }};
    
    assertEquals("null", mock.methodForNulls1("blablabla", new ArrayList<String>()));
    mock.methodForNulls2("blablabla", null);
    
    new Verifications() {{
        mock.methodForNulls2(anyString, (List<String>) withNull());
    }};
}

关键区别:null表示任意List,*withNull()*表示List的null引用。这种设计避免了类型转换(注意第三个参数需要强制转换而第二个不需要)。

使用前提:必须至少使用一个显式参数匹配器(with方法或any字段)。

2.4. "Times" 字段

有时需要限制模拟方法的调用次数,JMockit提供了timesminTimesmaxTimes关键字(三者均仅支持非负整数):

public interface ExpectationsCollaborator {
    void methodForTimes1();
    void methodForTimes2();
    void methodForTimes3();
}

@Test
public void testWithTimes(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodForTimes1(); times = 2;
        mock.methodForTimes2();
    }};
    
    mock.methodForTimes1();
    mock.methodForTimes1();
    mock.methodForTimes2();
    mock.methodForTimes3();
    mock.methodForTimes3();
    mock.methodForTimes3();
    
    new Verifications() {{
        mock.methodForTimes3(); minTimes = 1; maxTimes = 3;
    }};
}

在这个例子中:

  • times = 2 要求*methodForTimes1()*必须精确调用2次(不能多也不能少)
  • 默认行为(minTimes = 1)要求*methodForTimes2()*至少调用1次
  • minTimes = 1maxTimes = 3组合要求*methodForTimes3()*调用1-3次

注意:minTimesmaxTimes可同时使用(需先赋值minTimes),而times必须单独使用。

2.5. 自定义参数匹配

*当参数匹配需要复杂逻辑时,JMockit提供了with(Delegate)*方法**。

看一个通过类类型匹配对象的例子:

public interface ExpectationsCollaborator {
    void methodForArgThat(Object o);
}

public class Model {
    public String getInfo(){
        return "info";
    }
}

@Test
public void testCustomArgumentMatching(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodForArgThat(with(new Delegate<Object>() {
            public boolean matches(Object item) {
                return item instanceof Model && "info".equals(((Model) item).getInfo());
            }
        }));
    }};
    mock.methodForArgThat(new Model());
}

3. 返回值处理

现在讨论返回值相关内容:注意以下方法仅适用于Expectations,因为Verifications无法定义返回值。

3.1. Result和Returns(…)

在JMockit中,有三种方式定义模拟方法的返回值。这里介绍最常用的两种(覆盖90%日常场景):

  1. result字段:

    • 为非void方法定义单次返回值(也可抛出异常)
    • 多次赋值可实现多次调用的不同返回(可混合返回值和异常)
    • 赋值数组/列表可等效实现多次返回(类型需匹配,不支持异常
  2. *returns(Object…)*方法:

    • 语法糖,用于同时定义多个返回值

看代码更直观:

public interface ExpectationsCollaborator{
    String methodReturnsString();
    int methodReturnsInt();
}

@Test
public void testResultAndReturns(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodReturnsString();
        result = "foo";
        result = new Exception();
        result = "bar";
        returns("foo", "bar");
        mock.methodReturnsInt();
        result = new int[]{1, 2, 3};
        result = 1;
    }};

    assertEquals("Should return foo", "foo", mock.methodReturnsString());
    try {
        mock.methodReturnsString();
        fail("Shouldn't reach here");
    } catch (Exception e) {
        // NOOP
    }
    assertEquals("Should return bar", "bar", mock.methodReturnsString());
    assertEquals("Should return 1", 1, mock.methodReturnsInt());
    assertEquals("Should return 2", 2, mock.methodReturnsInt());
    assertEquals("Should return 3", 3, mock.methodReturnsInt());
    assertEquals("Should return foo", "foo", mock.methodReturnsString());
    assertEquals("Should return bar", "bar", mock.methodReturnsString());
    assertEquals("Should return 1", 1, mock.methodReturnsInt());
}

这个例子中:

  • 前三次调用methodReturnsString()依次返回"foo"、异常、"bar"(通过三次result赋值)
  • 第四、五次调用返回"foo"和"bar"(使用returns方法)
  • *methodReturnsInt()*通过数组赋值实现连续返回1,2,3,最后通过简单赋值返回1

3.2. 委托器

最后介绍第三种返回值定义方式:Delegate接口。当需要复杂返回逻辑时,这是最佳选择。

看个简单例子:

public interface ExpectationsCollaborator {
    int methodForDelegate(int i);
}

@Test
public void testDelegate(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodForDelegate(anyInt);
            
        result = new Delegate() {
            int delegate(int i) throws Exception {
                if (i < 3) {
                    return 5;
                } else {
                    throw new Exception();
                }
            }
        };
    }};

    assertEquals("Should return 5", 5, mock.methodForDelegate(1));
    try {
        mock.methodForDelegate(3);
        fail("Shouldn't reach here");
    } catch (Exception e) {
    }
}

使用方式:

  1. 创建Delegate实例并赋给result
  2. 在实例中定义与模拟方法参数和返回类型一致的方法(方法名任意)
  3. 在方法内实现自定义逻辑

本例中实现了:当参数<3时返回5,否则抛异常(注意:定义返回值后默认行为失效,需用times指定调用次数)

虽然代码量稍大,但在某些复杂场景下这是唯一解决方案。

4. 总结

到这里,我们已经覆盖了日常测试中创建Expectations和Verifications所需的核心技术。

我们还会发布更多JMockit相关文章,敬请关注。

完整代码实现见GitHub项目

4.1. 系列文章

系列文章列表:


原始标题:A Guide to JMockit Expectations