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()测试用例。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容