1. 概述
本文是JMockit系列文章的第二篇。建议先阅读第一篇基础教程,因为本文假设你已经掌握了JMockit的基本用法。
今天我们将深入探讨Expectations的核心机制,重点展示如何定义更精确或更通用的参数匹配,以及更高级的返回值定义方式。
2. 参数值匹配
以下方法同时适用于Expectations和Verifications。
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提供了times、minTimes和maxTimes关键字(三者均仅支持非负整数):
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 = 1和maxTimes = 3组合要求*methodForTimes3()*调用1-3次
注意:minTimes和maxTimes可同时使用(需先赋值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%日常场景):
result字段:
- 为非void方法定义单次返回值(也可抛出异常)
- 多次赋值可实现多次调用的不同返回(可混合返回值和异常)
- 赋值数组/列表可等效实现多次返回(类型需匹配,不支持异常)
*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) {
}
}
使用方式:
- 创建Delegate实例并赋给result
- 在实例中定义与模拟方法参数和返回类型一致的方法(方法名任意)
- 在方法内实现自定义逻辑
本例中实现了:当参数<3时返回5,否则抛异常(注意:定义返回值后默认行为失效,需用times指定调用次数)
虽然代码量稍大,但在某些复杂场景下这是唯一解决方案。
4. 总结
到这里,我们已经覆盖了日常测试中创建Expectations和Verifications所需的核心技术。
我们还会发布更多JMockit相关文章,敬请关注。
完整代码实现见GitHub项目。
4.1. 系列文章
系列文章列表: