TestableMock:解锁Java单元测试的Mock能力
TestableMock是阿里巴巴开发的一款轻量级Java单元测试Mock工具,它支持Mock公共方法、私有方法、静态方法以及通过new关键字创建的对象的方法等。
引入依赖
在使用TestableMock之前,需要在项目的构建文件中引入相关依赖。以Maven项目为例,可以在pom.xml文件中添加以下依赖:
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>最新版本号</version>
<scope>test</scope>
</dependency>
同时,还需要配置Maven插件以启用TestableMock的Java Agent:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${最新版本号}/testable-agent-${最新版本号}.jar</argLine>
</configuration>
</plugin>
</plugins>
</build>
创建Mock容器
在TestableMock中,可以通过多种方式创建Mock容器,包括注解、内部类和独立类。以下是每种方式的示例:
注解方式:
在测试类中使用注解声明Mock类。这种方式最为简洁,但要求Mock类与被测试类在同一个包路径下(除非使用了@MockWith注解指定Mock类)。
@MockWith(ServiceMock.class)
class TestServiceTest {
// 测试代码
}
其中,ServiceMock是一个独立的Mock类,定义了需要Mock的方法。
内部类方式:
在测试类中创建一个静态内部类,并在该类中定义Mock方法。这种方式适用于测试类与Mock方法紧密相关的情况。
class TestServiceTest {
public static class Mock {
@MockInvoke(targetClass = TestService.class)
private String privateMethod(String str) {
return "456";
}
}
// 测试方法
@ParameterizedTest
@CsvSource({"123", "456"})
void testPublicMethod(String str) {
String res = testService.publicMethod(str);
assertEquals("456", res);
}
}
独立类方式:
在测试类相同包路径下创建一个独立的Mock类。这种方式适用于多个测试类共享同一个Mock类的情况。
public class TestServiceMock {
@MockInvoke(targetClass = TestService.class)
private String privateMethod(String str) {
return "456";
}
// 其他Mock方法
}
class TestServiceTest {
// 测试代码
}
Mock方法的使用
Mock私有方法:
TestableMock支持直接Mock私有方法,这在某些情况下非常有用,特别是当私有方法通过公共方法间接调用且需要达到较高覆盖率时。
String res = PrivateAccessor.invoke(new TestService(), "privateMethod", "123");
Mock静态方法:
通过@MockMethod注解可以Mock静态方法。注意,这种方式需要确保Mock类与方法定义正确,并且测试运行时启用了TestableMock的Java Agent。
@MockMethod(targetClass = StaticService.class, targetMethod = "getStaticName")
private static String mockGetStaticName(MockContext context, String name) {
return "Mock Static " + name;
}
Mock通过new关键字创建的对象的方法:
对于通过new关键字直接创建的对象,TestableMock同样能够Mock其方法。这种方式需要确保Mock类与方法定义正确,并且测试运行时启用了TestableMock的Java Agent。
// 被测类
public class NewObjectService {
public String sayNewHello() {
NameService nameService = new NameService();
return nameService.getName("Charlie");
}
}
// 测试类
public class NewObjectServiceTest {
@MockMethod(targetClass = NameService.class, targetMethod = "getName")
private static String mockGetName(MockContext context, String name) {
return "Mock New " + name;
}
@Test
public void testNewObjectMethod() {
NewObjectService newObjectService = new NewObjectService();
String result = newObjectService.sayNewHello();
assertEquals("Mock New Charlie", result);
}
}
校验Mock调用
在测试中,除了需要将某些含有外部依赖的方法替换为Mock外,经常还需要验证该方法被调用时的参数是否符合预期。TestableMock提供了校验器(verifier)和匹配器(matcher)来实现这一功能。
基本校验器:
verify("mockMethod").with(123, "abc");
这个用例会检查在执行被测方法时,名称是mockMethod的Mock方法是否有被调用过,且调用时收到的参数值是否为123和"abc"。
with(Object... args):验证方法是否被指定参数调用过。withInOrder(Object... args):如果指定方法被调用了多次,依据实际调用顺序依次匹配。withTimes(int expectedCount):验证方法是否被调用过指定次数,忽略对调用参数的检查。without(Object... args):验证方法从未被使用指定参数调用过。times(int count):连在with()或withInOrder()方法之后使用,验证该方法被同样条件的参数调用过了指定次数。
基本匹配器:
verify("mockMethod").with(anyInt(), anyString());
这个用例会检查在执行被测方法时,名称是mockMethod的Mock方法是否有被调用过,且调用时收到的第一个参数是否为任意整数,第二个参数是否为任意字符串。
any():匹配任何值,包括Null。anyString():匹配任何字符串。anyNumber():匹配任何数值(整数或浮点数)。anyList():匹配任何列表。anySet():匹配任何集合。anyMap():匹配任何映射。eq(Object obj):匹配与指定值相等的对象。refEq(Object obj):匹配指定对象(非值相等,而是同一个对象)。
示例:Mock业务逻辑中的RPC调用
假设有一个业务类AService,其方法findByCondition依赖于RPC服务的结果。在编写单元测试时,可以使用TestableMock来Mock掉RPC调用的过程,并模拟不同的返回结果。
public class AService {
private RPCService rpcService;
public Boolean findByCondition(JsonObject query) {
RPCResult rpcResult = rpcService.query(query);
if (rpcResult.code.equals("Success")) {
// doSomething...
return true;
} else {
// doSomething...
return false;
}
}
}
public class AServiceTest {
private AService aService = new AService();
public static class Mock {
@MockInvoke(targetClass = RPCService.class, targetMethod = "query")
private RPCResult query(JsonObject query) {
switch ((String) MOCK_CONTEXT.get("RPC调用")) {
case "查询有结果":
List<String> list = new ArrayList<>();
list.put("1");
list.put("2");
list.put("3");
return new RPCResult(list);
case "查询结果为空":
return new RPCResult(new ArrayList<>());
case "RPC调用异常":
throw new NetworkException();
default:
return null;
}
}
}
@Test
public void testFindByCondition() {
MOCK_CONTEXT.put("RPC调用", "查询有结果");
assertEquals(true, aService.findByCondition(new JsonObject()));
MOCK_CONTEXT.put("RPC调用", "查询结果为空");
assertEquals(false, aService.findByCondition(new JsonObject()));
MOCK_CONTEXT.put("RPC调用", "RPC调用异常");
try {
aService.findByCondition(new JsonObject());
fail("Expected NetworkException to be thrown");
} catch (NetworkException e) {
// Expected exception
}
}
}
在这个示例中,通过MOCK_CONTEXT来设置不同的测试场景,并在Mock方法中根据不同的场景返回不同的结果。这样,就可以在不依赖实际RPC服务的情况下,对findByCondition方法进行充分的单元测试。
更多推荐


所有评论(0)