testable-mock/docs/zh-cn/doc/use-mock.md
2021-04-06 00:03:38 +08:00

9.5 KiB
Raw Blame History

快速Mock被测类的任意方法调用

在单元测试中Mock方法的主要作用是替代某些需要外部依赖执行过程耗时执行结果随机或其他影响测试正常开展却并不影响关键待测逻辑的调用。通常来说某个调用需要被Mock往往只与其自身特征有关而与调用的来源无关。

基于上述特点,TestableMock设计了一种极简的Mock机制。与以往Mock工具以作为Mock的定义粒度在每个测试用例里各自重复描述Mock行为的方式不同TestableMock让每个业务类被测类关联一组可复用的Mock方法集合使用Mock容器类承载并遵循约定优于配置的原则按照规则自动在测试运行时替换被测类中的指定方法调用。

实际规则约定归纳起来只有两条:

  • Mock非构造方法拷贝原方法定义到Mock容器类@MockMethod注解
  • Mock构造方法拷贝原方法定义到Mock容器类返回值换成构造的类型方法名随意@MockContructor注解

具体使用方法如下。

0. 前置步骤准备Mock容器

首先为测试类添加一个关联的Mock类型作为承载其Mock方法的容器最简单的做法是在测试类里添加一个名称为Mock的静态内部类。例如:

public class DemoTest {

    public static class Mock {
        // 放置Mock方法的地方
    }

}

1.1 覆写任意类的方法调用

在Mock容器类中定义一个有@MockMethod注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的targetClass参数指定该方法原本所属对象类型。

此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。

例如,被测类中有一处"something".substring(0, 4)调用我们希望在运行测试的时候将它换成一个固定字符串则只需在Mock容器类定义如下方法

// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"something"`类型为`String`
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {
    return "sub_string";
}

当遇到待覆写方法有重名时,可以将需覆写的方法名写到@MockMethod注解的targetMethod参数里这样Mock方法自身就可以随意命名了。

下面这个例子展示了targetMethod参数的用法,其效果与上述示例相同:

// 使用`targetMethod`指定需Mock的方法名
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
@MockMethod(targetClass = String.class, targetMethod = "substring")
private String use_any_mock_method_name(int i, int j) {
    return "sub_string";
}

有时在Mock方法里会需要访问发起调用的原始对象中的成员变量或是调用原始对象的其他方法。此时可以将@MockMethod注解中的targetClass参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数。

TestableMock约定,当@MockMethod注解的targetClass参数未定义时Mock方法的首位参数即为目标方法所属类型参数名称随意。通常为了便于代码阅读建议将此参数统一命名为selfsrc。举例如下:

// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@MockMethod
private String substring(String self, int i, int j) {
    // 可以直接调用原方法此时Mock方法仅用于记录调用常见于对void方法的测试
    return self.substring(i, j);
}

完整代码示例见java-demokotlin-demo示例项目中的should_mock_common_method()测试用例。(由于Kotlin对String类型进行了魔改Kotlin示例中将被测方法在BlackBox类里加了一层封装)

1.2 覆写被测类自身的成员方法

有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉比如这个方法里有许多外部依赖或耗时操作

做法与前一种情况完全相同,只需将targetClass参数赋值为被测类,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。

例如,被测类中有一个签名为String innerFunc(String)的私有方法我们希望在测试的时候将它替换掉则只需在Mock容器类定义如下方法

// 被测类型是`DemoMock`
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
    return "mock_" + text;
}

同样的,上述示例中的方法如需访问发起调用的原始被测对象,也可不使用targetClass参数而是在定义Mock方法时在方法参数列表首位加一个类型为DemoMock的参数(名字随意)。

完整代码示例见java-demokotlin-demo示例项目中的should_mock_member_method()测试用例。

1.3 覆写任意类的静态方法

对于静态方法的Mock与普通方法相同。

例如,在被测类中调用了BlackBox类型中的静态方法secretBox(),该方法签名为BlackBox secretBox()则Mock方法如下

@MockMethod(targetClass = BlackBox.class)
private BlackBox secretBox() {
    return new BlackBox("not_secret_box");
}

对于静态方法的Mock通常不使用方法参数列表的首位加参数来表示目标类型。但这种方法也依然适用只是实际传入的第一个参数值将始终是null

完整代码示例见java-demokotlin-demo示例项目中的should_mock_static_method()测试用例。

1.4 覆写任意类的new操作

在Mock容器类里定义一个返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致的方法名称随意然后加上@MockContructor注解。

此时被测类中所有用new创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。

例如,在被测类中有一处new BlackBox("something")调用希望在测试时将它换掉通常是换成Mock对象或换成使用测试参数创建的临时对象则只需定义如下Mock方法

// 要覆写的构造函数签名为`BlackBox(String)`
// Mock方法返回`BlackBox`类型对象,方法的名称随意起
@MockContructor
private BlackBox createBlackBox(String text) {
    return new BlackBox("mock_" + text);
}

完整代码示例见java-demokotlin-demo示例项目中的should_mock_new_object()测试用例。

2. 在Mock方法中区分调用来源

在Mock方法中通过TestableTool.SOURCE_METHOD变量可以识别进入该Mock方法前的被测类方法名称;此外,还可以借助TestableTool.MOCK_CONTEXT变量为Mock方法注入“额外的上下文参数”,从而区分处理不同的调用场景。

例如在测试用例中验证当被Mock方法返回不同结果时对被测目标方法的影响

@Test
public void testDemo() {
    MOCK_CONTEXT.put("case", "data-ready");
    assertEquals(true, demo());
    MOCK_CONTEXT.put("case", "has-error");
    assertEquals(false, demo());
}

在Mock方法中取出注入的参数根据情况返回不同结果

@MockMethod
private Data mockDemo() {
    switch((String)MOCK_CONTEXT.get("case")) {
        case "data-ready":
            return new Data();
        case "has-error":
            throw new NetworkException();
        default:
            return null;
    }
}

完整代码示例见java-demokotlin-demo示例项目中的should_get_source_method_name()should_get_test_case_name()测试用例。

3. 验证Mock方法被调用的顺序和参数

在测试用例中可用通过InvokeVerifier.verify()方法,配合with()withInOrder()without()withTimes()等方法实现对Mock调用情况的验证。

详见校验Mock调用文档。

4. 特别说明

Mock只对被测类的代码有效

TestableMockIssues列表中最常见的一类问题是“Mock为什么没生效”其中最多的一种情况是“在测试用例里直接调用了Mock的方法发现没有替换”。这是因为Mock替换只会作用在被测类的代码里。知道大家是想快速验证一下TestableMock的功能(这心意我们领了👻不过测试用例的代码真的无需被Mock哦(~ ̄▽ ̄)~。

除去这种情况若Mock未生效请参考自助问题排查提供的方法对比Mock方法签名目标位置的调用方法签名。若依然无法定位原因欢迎提交Issues告诉我们。

测试类和Mock容器的命名约定

默认情况下,TestableMock假设测试类与被测类的包路径相同,且名称为被测类名+Test(通常采用MavenGradle构建的Java项目均符合这种惯例。 同时约定测试类关联的Mock容器为在其内部且名为Mock的静态类,或相同包路径下名为被测类名+Mock的独立类

当测试类或Mock容器路径不符合此约定时可使用@MockWith注解显式指定,详见使用MockWith注解

关于TestableMock的更多实现细节可参考设计和原理文档。