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方法进行充分的单元测试。

更多推荐