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

时间:2022-04-27
本文章向大家介绍糖大夫--测量流程性能监控自动化方案设计,主要内容包括背景、整体时序图和Demo、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

糖大夫(简称)是一款血糖仪(想了解更多的同学请看这里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();
                    }
                }
            }
        });

    }