糖大夫--测量流程性能监控自动化方案设计

糖大夫(简称)是一款血糖仪(想了解更多的同学请看这里http://tdf.qq.com/),但不止血糖仪。血糖仪终端具备触屏、联网、高准度血糖检测单元。除了终端之外,和它配合的还有微信端、医生端。微信端重家属属性,因糖尿病不可治愈,长期的管理中,家庭关怀是重要的一个环节,在患者无法坚持的时候,家庭给予有力的支持。医生端供医生远程了解患者血糖数据,并给予专业指导。

背景

故事的起源源自一次晨会,开发总监在会上主动提出把糖大夫测量流程性能做成日常监控,而碰巧我规划的下半年计划中就囊括了糖大夫自动化,那还在等什么?Just do it!

技术可行性预研

糖大夫测量流程操作步骤依次可以分解为:

1、进入测血糖页面

2、插入试纸

3、校验试纸(是否为已使用过试纸、是否为符合该血糖仪的试纸等)

4、校验通过后自动切换到采血页提示用户滴血

5、用户滴血

6、血糖模组计算血液中血糖浓度

7、血糖模组返回本次测量值给糖大夫app,app从测量过程页自动切换到结果页,并在结果页显示血糖测量值

其中只有步骤1、2、5是用户操作,步骤3、6由底层血糖硬件模组完成,步骤4、7的页面切换以及步骤3、6的检测都是由模组完成后,模组检测后,返回数据给糖大夫app,app根据返回的数据做页面切换

现在遇到了第一个问题---整个测量流程完全依赖于底层的硬件模组,单纯的自动化脚本是无法走完这个流程(单纯的自动化脚本只能进入测血糖页面),那能不能找到一种方法来mock硬件模组和糖大夫app通信,从而通过代码来跳过整个测量流程?

抱着这个目的,首先来看看源代码中糖大夫app和模组之间具体是如何进行通信的,通过阅读项目代码,具体的通信流程如下:

首先血糖模组是一个独立的硬件,模组把一些用户操作(试纸插入/滴血/)转换成数据,然后通过串口和糖大夫app进行通信

而糖大夫通信层负责数据接收,并提供了回调接口供业务层注册,业务层向通信层注册Handler,当通信层接收到模组传递的各类数据后(如试纸插入、采血等),通过注册Handler通知业务层各类事件的发生,而测血糖页面(单Activity+多Fragment)注册了对应的Handler,并在Handler.handleMessage回调函数中,处理各类页面跳转等页面切换操作

通过在采血页加入测试模拟代码拿到采血页注册的Handler,再通过Handler模拟发送各类硬件消息,可以完全跳过整个测量流程,看来从技术上来说是完全可行的!

那么现在又遇到新的问题,测试模拟代码在采血页内部执行,很容易拿到采血页注册的Handler对象,但是测试代码不在采血页内部,并且Activity的创建是由系统完成的,如何拿到测量页Activity实例内的Handler?

好的是(好吧,我承认之前做过类似的hook,所以这里跳转的比较突然),google已经帮我们想到了这个问题,在android4.0以上(4.0以下通过替换ActivityThread内静态Instrumentation类对象来实现),在Application类中提供了一个Activitylifecyclecallbacks接口,接口内回调函数和Activity各生命周期回调一一对应,并且每个回调函数均带有Activity参数,注册后,可以拿到本App内所有的Activity实例

    public interface ActivityLifecycleCallbacks {        
    void onActivityCreated(Activity var1, Bundle var2);       
    void onActivityStarted(Activity var1);      
    void onActivityResumed(Activity var1);       
    void onActivityPaused(Activity var1);        
    void onActivityStopped(Activity var1);       
    void onActivitySaveInstanceState(Activity var1, Bundle var2);      void onActivityDestroyed(Activity var1);
    }

通过测试代码实现这个接口并在Application中注册,然后通过instance of判断是否在采血页,获取到采血页Activity实例后,拿到Handler,然后模拟试纸插入、滴血、测量结束三类消息,自动化跳过了整个测量流程

开发设计以及工具选型

技术可行性验证通过,那么如何来设计整体架构,自动化脚本通过何种操作app内测试代码?它们之间又是如何通信?

对于自动化脚本来说,它并不关心通信的细节,而且如果暴露了通信细节,反而会加大自动化脚本的开发难度,所以通过封装成SDK的形式来屏蔽通信细节,自动化脚本只需要关注业务即可

SDK设计

在sdk设计中,把一次通信流程抽象为的Request/Response形式,并根据自动化测试业务场景,设计为同步请求(必须要等这个场景完成后才能进入自动化脚本下一步)、异步请求(如添加白名单这种不依赖返回值的操作)两种方式

而SDK本身架构设计,并没有太多东西,只需要做好分层设计,方便后续扩展以及维护即可

在稳定性上,反而需要重点关注,SDK内所有线程都实现了Thread.UncaughtExceptionHandler接口,防止未处理异常外逃到自动化脚本,从而导致自动化脚本crash

糖大夫APP内测试接口设计

在糖大夫APP这一侧,结合已有工具并考虑到后续糖大夫项目会加入越来越多的自动化,通信的接口会越来越多,这一部分必须要易扩展,易维护

通过代码分层设计,测试代码从下到上设计成与业务无关的通信层、负责请求转发的控制层、与业务耦合的逻辑层这种常见架构

同时,考虑到测试代码和开发代码同处于一个工程,必须保证测试代码不影响正式代码的稳定性和安全性,其次糖大夫本身未做分包处理,如果测试代码以及测试代码引入的第三库代码过多,那么很容易超过64K方法数限制;针对上述问题,做了以下措施规避:

1、测试代码放到专门的test包下,通过测试代码和开发代码分属不同的包来实现物理隔离,再通过编译打包控制测试代码不被打进去

2、开发代码中调起测试代码部分(在Application onCreate中调起测试代码),全部使用基类接口引用,并通过反射的方式加载,以防止打正式包出现编译错误

3、除了必须暴露的接口,所有测试接口访问权限均为private,并添加对应的注释,以防止开发人员误调测试接口(这部分主要针对开发代码中调起测试接口部分)

4、所有测试代码,吃掉全部异常,防止触发app内crash上报机制,误报crash

SDK和糖大夫APP进程间通信方式选型

自动化脚本和糖大夫app内测试代码,分处不同的进程,那么他们通过何种进程间通信方式来实现数据交换? 在android中,应用层app常见的通信方式有以下几种:

从编程角度来说,使用广播是最简单,但是广播的缺点很明显---只支持单向通信

不过,既然我们已经设计成sdk这种形式,完全可以通过让sdk和app各注册一个广播的形式来模拟双向通信(基于Uiautomator2的自动化脚本,能拿到Context对象,能很方便的注册广播)

通信协议格式设计

android中,通过binder跨进程传递的数据,只能是基本类型、String、实现了Serializable接口或者Parcelable接口的复合类型,考虑到序列化/反序列化操作难以程度,读取/解析效率,以及后续的可扩展性,选用了json这一常见数据交换方式作为通信协议载体(json和String可以很容易相互转换,json增加一个字段后,除了更改增加字段接口的读取和发送外,其他地方均不需要更改),针对每一个测试接口(比如跳过血糖流程、导入血糖数据),分配一个固定的全局唯一的cmd号

具体的协议格式如下(协议格式)

请求格式:
{    "cmd": 1000, ##要访问的全局唯一接口号
    "request data": {} ##请求的数据,可为空}


响应格式:
{    "cmd": 1000, ##本次响应的接口号
    "status": 1, ##请求状态(成功为0,非0为失败)
    "error msg": "", ##错误信息
    "result data": {} ##响应的数据,可为空}

自动化框架及平台选型

在自动化框架方面,因为糖大夫本身是基于android4.4.2编译,可以完美支持UIautomator2,所以选取UIautomator2作为自动化测试脚本框架

在性能监控以及调度展示平台方面,沿用测试组内部使用的工具/平台即可

整体时序图和Demo

Demo代码如下

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {   
private static UiDevice sDevice;    

@BeforeClass
    public static void beforeTest() {
        SugarSdk.init(InstrumentationRegistry.getTargetContext().getApplicationContext());

        sDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        sDevice.pressHome();
    }    
@Test
    public void testSugar() {
        UiObject testBtn = sDevice.findObject(new UiSelector().resourceId("com.tencent.sugardoctor:id/btn_blood_sugar_test"));        try {
            testBtn.click();
        } catch (UiObjectNotFoundException e) {
            e.printStackTrace();
        }

        SkipSugarTestFlow skipSugarTest = new SkipSugarTestFlow();
        skipSugarTest.setRequestSugarResult(11.2);
        skipSugarTest.waitForSkipSugarTestFlowFinished();
        assertEquals(11.2, skipSugarTest.getSugarAppReturnedSugarResult(), 0.001);
    }

针对糖大夫APP内接口更新,可以不更新SDK的情况下兼容新接口

 /**针对糖大夫app已提供新接口,但SDK还未更新的情况下,自动化脚本可以兼容新接口
     *
     * 同步请求方式
     * **/
    @Test
    public void testSugarSync() {
        UiObject testBtn = sDevice.findObject(new UiSelector().resourceId("com.tencent.sugardoctor:id/btn_blood_sugar_test"));        try {
            testBtn.click();
        } catch (UiObjectNotFoundException e) {
            e.printStackTrace();
        }

        RequestQueue requestQueue = SugarSdk.getRequestQueue();
        JSONObject requestData = new JSONObject();        try {
            requestData.put("sugarResult", 11.2);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        JsonObjectRequest request = new JsonObjectRequest(10001, requestData);
        SyncRequest syncRequest = requestQueue.addSyncRequest(request);
        Response resp = syncRequest.waitForResponse();        try {            double sugarResult = resp.getResultData().getDouble("result");
            assertEquals(11.2, sugarResult, 0.001);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }    /**针对糖大夫app已提供新接口,但SDK还未更新的情况下,自动化脚本可以兼容新接口
     *
     * 异步请求方式
     * **/
    @Test
    public void testSugarAsync() {
        UiObject testBtn = sDevice.findObject(new UiSelector().resourceId("com.tencent.sugardoctor:id/btn_blood_sugar_test"));        try {
            testBtn.click();
        } catch (UiObjectNotFoundException e) {
            e.printStackTrace();
        }

        RequestQueue requestQueue = SugarSdk.getRequestQueue();
        JSONObject requestData = new JSONObject();        try {
            requestData.put("sugarResult", 11.2);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        JsonObjectRequest request = new JsonObjectRequest(10001, requestData);
        requestQueue.addAsyncRequest(request, new AsyncRequest.ResponseListener() {            @Override
            public void onResponse(Response response) {                if(response.isSuccessed()) {                    try {                        double sugarResult = response.getResultData().getDouble("result");
                        assertEquals(11.2, sugarResult, 0.001);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

    }

原文发布于微信公众号 - 腾讯移动品质中心TMQ(gh_2052d3e8c27d)

原文发表时间:2016-10-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

如何利用Microsoft Edge漏洞获取本地文件?

在2015年,微软发布了Edge浏览器。当它最初被开发时,它被命名为Project Spartan。

12120
来自专栏吾爱乐享

php学习之html案例(九)

17820
来自专栏草根专栏

使用 Moq 测试.NET Core - Why Moq?

在一个项目里, 我们经常需要把某一部分程序独立出来以便我们可以对这部分进行测试. 这就要求我们不要考虑项目其余部分的复杂性, 我们只想关注需要被测试的那部分. ...

16830
来自专栏杨建荣的学习笔记

crontab导致的频繁发送邮件的问题(r5笔记第20天)

今天下午的时候客户发邮件反馈说,对于某个环境中的文件系统监控和表空间使用情况的邮件收到的比较频繁,感觉是1个小时发送一次,完全可以3个小时发送一次,接到这个问题...

33240
来自专栏西枫里博客

关机后远程唤醒的配置,简单实现广域网远程开机和连接

出门在外经常需要家里或者办公室电脑里面的资料。通常通过远程桌面等控制类软件连接。当家里没人,没人开电脑就麻烦了,如果让家里电脑始终开着浪费能源,所以远程桌面...

51820
来自专栏FreeBuf

老式后门之美:五种复古远程控制工具

目前,高级的rootkit技术可以帮助你在渗透中拿到一个shell后进行持久性控制。此外,还有供应商故意提供了一些植入后门,但那就是截然不同的故事了。尽管各式花...

27790
来自专栏技巅

linux内核崩溃问题排查过程总结

63140
来自专栏女程序员的日常

SSD固态硬盘的GC与Trim

操作系统:其实并没有删除数据;  事实上,它只是在硬盘前的索引区里标记这块文件占用的区域为无效的,  所以等该区域被擦除后,下次数据将要再次写入的时候,可以写入...

28810
来自专栏微服务生态

深入讨论阻塞与非阻塞、同步与异步的区别

异步:某个事情需要10s完成。而我只需要调用某个函数告诉xxx来帮我做(然后我再干其他的事情)

9120
来自专栏Keegan小钢

App架构经验总结(三)

原文链接:http://keeganlee.me/post/architecture/20160303 版权声明:本文刊载在《程序员》杂志2016年3期,版权归...

15150

扫码关注云+社区

领取腾讯云代金券