TestableMock 单元测试 Mock 工具

git:
https://github.com/alibaba/testable-mock
文档:
https://alibaba.github.io/testable-mock/#/

单测工具于我而言的作用,就是可以对关键节点有测试,保证长期开发或重构的稳定和正确性;
操作上越简单易用越好,越节省开发时间越好;TestableMock是个较好的选择;
TestableMock是利用字节码增强技术,来进行Mock的工具; 阅读上述文档基本可以做到快速上手;它主要优点:

  • 访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
  • 快速Mock任意调用:使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
  • 辅助测试void方法:利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题

TestableMock简化设计主要基于两条基本假设:
假设一:同一个测试类里,一个测试用例里需要Mock掉的方法,在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
假设二:需要Mock的调用都来自被测类的代码。此假设是符合单元测试初衷的,即单元测试只应该关注当前单元的内部行为,单元外的逻辑应该被替换为Mock。

源码中有demo模块,其中使用方式还是很详细的;这里简单列举下常用方法,感兴趣可以用起来:

  1. 来测些调用外部RPC接口的方法;
    RPC接口
public interface WeatherApi {
    @RequestLine("GET /api/weather/city/{city_code}")
    WeatherExample.Response query(@Param("city_code") String cityCode);
}

被测类

public class CityWeather {
    private static final String API_URL = "http://t.weather.itboy.net";
    private static final String BEI_JING = "101010100";
    private static final String SHANG_HAI = "101020100";
    private static final String HE_FEI = "101220101";
    public static final Map<String, String> CITY_CODE = MapUtil.builder(new HashMap<String, String>())
            .put(BEI_JING, "北京市")
            .put(SHANG_HAI, "上海市")
            .put(HE_FEI, "合肥市")
            .build();
    private static WeatherApi weatherApi = Feign.builder()
            .encoder(new JacksonEncoder())
            .decoder(new JacksonDecoder())
            .target(WeatherApi.class, API_URL);
    public String queryShangHaiWeather() {
        WeatherExample.Response response = weatherApi.query(SHANG_HAI);
        return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice();
    }
    private String queryHeFeiWeather() {
        WeatherExample.Response response = weatherApi.query(HE_FEI);
        return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice();
    }
    public static String queryBeiJingWeather() {
        WeatherExample.Response response = weatherApi.query(BEI_JING);
        return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice();
    }
    public static void main(String[] args) {
        CityWeather cityWeather = new CityWeather();
        String shanghai = cityWeather.queryShangHaiWeather();
        String hefei = cityWeather.queryHeFeiWeather();
        String beijing = CityWeather.queryBeiJingWeather();
        System.out.println(shanghai);
        System.out.println(hefei);
        System.out.println(beijing);
    }

测试类

@EnablePrivateAccess
public class CityWeatherTest {
    @TestableMock(targetMethod = "query")
    public WeatherExample.Response query(WeatherApi self, String cityCode) {
        WeatherExample.Response response = new WeatherExample.Response();
        // mock天气接口调用返回的结果
        response.setCityInfo(new WeatherExample.CityInfo().setCity(
                CityWeather.CITY_CODE.getOrDefault(cityCode, cityCode)));
        response.setData(new WeatherExample.Data().setYesterday(
                new WeatherExample.Forecast().setNotice("this is from mock")));
        return response;
    }
    CityWeather cityWeather = new CityWeather();
    /**
     * 测试 public方法调用
     */
    @Test
    public void test_public() {
        String shanghai = cityWeather.queryShangHaiWeather();
        System.out.println(shanghai);
        assertEquals("上海市: this is from mock", shanghai);
    }
    /**
     * 测试 private方法调用
     */
    @Test
    public void test_private() {
        String hefei = (String) PrivateAccessor.invoke(cityWeather, "queryHeFeiWeather");
        System.out.println(hefei);
        assertEquals("合肥市: this is from mock", hefei);
    }
    /**
     * 测试 静态方法调用
     */
    @Test
    public void test_static() {
        String beijing = CityWeather.queryBeiJingWeather();
        System.out.println(beijing);
        assertEquals("北京市: this is from mock", beijing);
    }
}
  1. 调用外部方法的void方法
    例如,下面这个方法会根据输入打印信息到控制台:
class Demo {
    public void recordAction(Action action) {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
        String timeStamp = df.format(new Date());
        System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
    }
}

若要测试此方法,可以利用TestableMock快速Mock掉System.out.println方法。在Mock方法体里可以继续执行原调用(相当于并不影响本来方法功能,仅用于做调用记录),也可以直接留空(相当于去除了原方法的副作用)。

在执行完被测的void类型方法以后,用InvokeVerifier.verify()校验传入的打印内容是否符合预期:

class DemoTest {
    private Demo demo = new Demo();

    // 拦截`System.out.println`调用
    @MockMethod
    public void println(PrintStream ps, String msg) {
        // 执行原调用
        ps.println(msg);
    }

    @Test
    public void testRecordAction() {
        Action action = new Action("click", ":download");
        demo.recordAction();
        // 验证Mock方法`println`被调用,且传入参数符合预期
        verify("println").with(matches("\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[click\] :download"));
    }
}

3.还有些好用的功能例如,识别当前测试用例和调用来源

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

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

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

在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;
    }
}

注意,由于TestableMock并不依赖(也不希望依赖)任何特定测试框架,因而无法自动识别单个测试用例的结束位置,这使得设置到TestableTool.MOCK_CONTEXT变量的参数可能会在同测试类中跨测试用例存在。建议总是在使用后及时使用MOCK_CONTEXT.clear()清空上下文

在当前版本中,此变量在运行期的效果类似于一个在测试类中的普通Map类型成员对象,但请尽量使用此变量而非自定义对象传递附加的Mock参数,以便在将来升级至v0.5版本时获得更好的兼容性。

TestableTool.MOCK_CONTEXT变量的值是在测试类内共享的,当单元测试并行运行时,建议请选择parallel类型为classes

完整代码示例见java-demo和kotlin-demo示例项目中的should_able_to_get_source_method_name()和should_able_to_get_test_case_name()测试用例。

推荐阅读更多精彩内容