Mock你的数据,放个大招

前言

本文提供一种实现Mock数据的解决方案,解决思路为核心,实现则以OkHttp3为例,可根据提供的思路将方案嵌入实际项目代码中。

为何要Mock

开发项目中,不管是等待API的完成,还是测试API正常、异常流是否达到预期,都需要相应的响应数据验证。但是等待他人提供API是痛苦的,Mock数据也痛苦的。

常见解决方案

达到目标可以有很多种路径,Mock方案也有很多种。常见的Mock方案有:

硬编码
比如有一个API需要测试某JSON数据,则将此String在回调处进行替换以达到目的。优点为操作方便,缺点如:

  • 调整需要重新编译
  • String过长可能无法完全复制
  • 如有多个API需要进行Mock,杂乱无章

工具映射
借助如Charles等工具可以将API等信息进行替换,优点为覆盖面广,可实时Mock。缺点为:

  • 有操作成本
  • 需要记住哪些API进行了Mock并在不需要时关闭功能

本地仓库
本地存有Mock数据文件,可网络层等合适位置对API进行查阅操作,命中时替换成Mock数据。优点为易维护,全面。缺点为:

  • 调整困难,需要重新打包
  • 可通过adb shell等方式动态替换数据文件,但操作有成本

放个大招

理想状态下,Mock应该满足以下要求:

  1. 不需要对已有程序进行改动,因为需要进行回退
  2. 易于管理API,Mock哪些API一目了然,可配置
  3. 实时操作,并且操作成本低

综合以上需要,给出方案思路大致如图


Mock方案.png

图中表达如下

  1. 方案嵌入位置选在网络层中的拦截层,得益于拦截层得天独厚的位置,在拦截层中实现Mock方案最方便,也最透明。了解拦截层点我。本篇文章的实现也是基于OkHttp3的拦截链来进行的,当然,在实际项目中拥有自主网络框架的拦截层再好不过。如果没有拦截层,也可以参考这个思路,实现在合适的位置。因为网络请求一定会有某个公用的入口和出口,在此类位置进行即可。
  2. 需要理解的一点是,Mock不代表没有网络请求,对于Mock来说,最关心的是拿到Mock数据,从何而来倒不是核心。
  3. 本地会有一份配置表,配置表记录的哪些API需要进行Mock和Mock地址。这份配置表不需要使用文件存储,实现某个共享对象如单例存储即可。而配置表的获取可以选择合适的时机从远端同步,比如程序启动时、发生网络请求时做预检查。
  4. 网络请求会被拦截层间层,当API在配置表中命中,则这证明此API需要Mock,将实际请求重定向为配置表配置的Mock地址从远端拿到Mock数据即可,反之,API未命中则维持原先操作。
  5. 各个Mock地址则保存的合适数据,保证实时性。

如此实现的原因则是综合了之前常见解决方案的优缺点:

  • 进行Mock不需要修改程序代码
  • 易于操作,需要进行Mock的API在远端配置即可
  • 配置更变即配置表有改动时,不需要重新打包,重启应用即可
  • 有实时性,在不需要进行Mock时,Debug环境下仅更改配置表即可
  • 更新Mock的API数据时,重新触发请求即可

如上所诉,需要一个远端仓库存储Mock数据和配置表,如果没有后端条件或者没有合适方案,可以使用Easy-Mock 点我,文章会基于此进行实现,如何使用Easy-Mock参考官方文档即可。

实现

首先说明,实现部分,代码不多,仅摘取了核心代码进行解说,因为涉及到需要依赖具体环境进行的代码,则以一个函数进行表达,代码中会做详细注释。

假设已拥有远端仓库,则配置表是这样子的

{
  "open": "true",
  "reflects": [{
      "url": "www.baidu.com",
      "mockKey": "baidu"
    },
    {
      "url": "text",
      "mockKey": "test"
    }
  ]
}

open字段代表Mock方案是否启用,reflects是映射数据,其中url代表需要进行mock的API,mockKey则代表进行拼接的key。以Easy-Mock来说,会提供基本url并通过不同的key来访问具体的Mock数据,假设基本url为“www.test.com”,则当mockKey为test时,mock地址为“www.test.com/test”。

而mock地址返回的数据,则视具体情况而定,比如下面一个简单的JSON。

{
  "code": 200,
  "message": "success",
  "entry": "your data"
}

如果是使用了Easy-Mock的话,可以类似这样的仓库图。


easy-mock api列表.jpg

仓库准备完毕,以下为核心代码

public class OKMockInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {

        // 获取原请求
        Request request = chain.request();

        // mock 条件,是具体情况而定
        if (isMock()){

            // 存储且管理远端配置表的数据,url检测也是通过此类检查
            MockWarehouse mockWarehouse = MockWarehouse.instance();

            /**
             * 拉取配置表
             */
            if (mockWarehouse.isInit()){
                // 配置表地址
                String mockConfigUrl = mockWarehouse.getConfigUrl();
                // 拉取配置表的请求
                Request configRequest = new Request.Builder()
                        .url(mockConfigUrl)
                        .build();

                // 拿到配置表信息
                Response configResponse = chain.proceed(configRequest);
                
                
                // 检查Response状态以及Response结果
                if (configResponse.code() == 200){
                    // 这里拿到response的string,
                    // 在okHttp3中,使用peekBody()而不是body()获取ResponseBody,因为后者会导致
                    // 流关闭,框架校验不通过。
                    String configInfo  = configResponse.peekBody(Integer.MAX_VALUE).string();
                    Log.d("mMock", "mockUrl : " + configInfo);
                    
                    // 这一步校验是希望配置表满足某些格式,视具体情况
                    if (configInfo.contains(MockWarehouse.IDENTIFY)){
                        // 更新MockWarehouse
                        mockWarehouse.updateMockReflect(configResponse.peekBody(Integer.MAX_VALUE).string());
                    }
                    
                } else {
                    // 拉不到配置,直接做原先的请求
                    return chain.proceed(request);
                }

            }

            /**
             * 尝试获取mock 的url
             */
            String mockUrl = mockWarehouse.getMockUrl(request.url().toString());

            if (!TextUtils.isEmpty(mockUrl)){
                Log.d("mMock", "mockUrl : " + mockUrl);

                // 构造远端mock的请求
                Request mockRequest = new Request.Builder()
                        .url(mockUrl)
                        .build();

                //拿到远端mock请求的结果
                Response mockResponse = chain.proceed(mockRequest);
                // 远端mock状态
                if (mockResponse.code() == 200){
                    String responseString = mockResponse.peekBody(Integer.MAX_VALUE).string();
                    // 校验response
                    if (!TextUtils.isEmpty(responseString)){
                        Log.d("mMock", "response : " + responseString);

                        // mock数据成功, 返回mock的response
                        return mockResponse;
                    }
                }
            }
        }

        // 无需mock或者mock不成功,返回原来的请求
        return chain.proceed(request);
    }

    /**
     * 进行mock的环境条件,视具体情况而定
     * @return
     */
    private boolean isMock(){
        return true;
    }

}

实现中需要注意几点:

  1. isMock()视具体情况而定,比如Release包不需要进行mock
  2. 可见在此拦截器中,会根据情况的不同进行了若干次请求,对于上层来说,关心的依旧是Reponse,从何而来是透明的
  3. 需要类似MockWarehouse存储远端配置表信息并负责做URL校验
  4. MockWarehouse的配置表信息同步时机需要进行考虑,视具体情况而定是否需要考虑并发问题
  5. 可以的话,希望Reponse能保持格式化

总结

以上完成了Mock方案,其中思路可根据实际需要进行实现,核心为:

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

推荐阅读更多精彩内容