接口测试 AshenAndroid — Android SDK 测试

小抄 · 2021年03月26日 · 最后由 TD 回复于 2022年02月22日 · 12813 次阅读

背景

目前市面上有很多的 SDK,开发者开发 App 时可以集成各种 SDK 来方便快捷的实现某一部分功能

最近做了 Andorid 和 iOS 的 SDK 测试,做了个小框架,共享出来,和大家一起探讨学习

SDK 的测试方法

目前大部分的 SDK 测试都是开发一个 demo,集成 SDK,然后针对这个 demo 测试以此传参到 SDK 的 api 中.

寻找其他的测试手段,先将 SDK 集成到 demo 中,demo 内置一个 HTTP server,demo 启动时反射扫描被测的 SDK 类,启动 HTTP server,测试人员以约定的参数格式发送 HTTP 请求到 demo 的 server 中,server 反射调用 SDK 对应的接口,返回 同步/异步 接口的返回值,有了返回值就可以用来做断言啦.

AshenAndroid

· 反射扫描被测类

· 根据 http 请求来自动映射对应的接口

· 支持测试同步/异步接口

· https://gitee.com/xiao-chao/AshenAndroid ⭐

· https://github.com/xiaochaom/AshenAndroid ⭐

· iOS SDK 接口测试 AsheniOS

接入要求

  1. 需要有 JDK 1.8 编译的 SDK, Android Studio 编译时,在项目的 build.gradle 文件最后增加
tasks.withType(JavaCompile) {
    options.compilerArgs += ["-parameters"]
}

这样生成的 jar 包,接口中会保留参数形参,反射获取到的为 String name,否则获取的参数为 String arg0,参数名会影响到测试用例的参数,所以显示出真实的参数名会提升用例的维护难易度,可想而知一个多参数的接口,用例写成{"arg0":"testerhome","arg1":"testerName"},维护起来多棘手.

接入方法

  1. 在项目中引入 SDK.
  2. 注册被测类, 在 utils/Init.java 文件中的 initClassRegister 函数注册:

    @RequiresApi(api = Build.VERSION_CODES.P)
    public void initClassRegister() {
        // 只需给 classRegistered put 就可以,key 为包路径,value 为调用 api 的 SDK 实例,单例类为 instance,例如 Gson
        classRegistered.put("com.google.gson.Gson", new Gson());
    
        for (Map.Entry classEntry : classRegistered.entrySet()) {
            Object o = classEntry.getValue();
            classObjectCallBack.putAll(ClassUtil.getClassObjectNotCallBack(o));
            classObject.putAll(ClassUtil.getClassObject(o));
            classTypeObject.putAll(ClassUtil.getClassTypeObject(o));
        }
    }
    

    此时可以打包安装 APP, app 上会显示手机的 host,例如 192.168.1.100 , 使用浏览器访问 http://192.168.1.100:9999/getAllInterface,就会显示出已注册类的所有函数,以上 Gson 的例子会返回

    {"com.google.gson.Gson":[{"equals":{"arg0":"java.lang.Object"}},{"excluder":{}},{"fieldNamingStrategy":{}},{"fromJson":{"arg0":"com.google.gson.JsonElement","arg1":"java.lang.Class<T>"}},{"fromJson":{"arg0":"com.google.gson.JsonElement","arg1":"java.lang.reflect.Type"}},{"fromJson":{"arg0":"com.google.gson.stream.JsonReader","arg1":"java.lang.reflect.Type"}},{"fromJson":{"arg0":"java.io.Reader","arg1":"java.lang.Class<T>"}},{"fromJson":{"arg0":"java.io.Reader","arg1":"java.lang.reflect.Type"}},{"fromJson":{"arg0":"java.lang.String","arg1":"java.lang.Class<T>"}},{"fromJson":{"arg0":"java.lang.String","arg1":"java.lang.reflect.Type"}},{"getAdapter":{"arg0":"com.google.gson.reflect.TypeToken<T>"}},{"getAdapter":{"arg0":"java.lang.Class<T>"}},{"getClass":{}},{"getDelegateAdapter":{"arg0":"com.google.gson.TypeAdapterFactory","arg1":"com.google.gson.reflect.TypeToken<T>"}},{"hashCode":{}},{"htmlSafe":{}},{"newBuilder":{}},{"newJsonReader":{"arg0":"java.io.Reader"}},{"newJsonWriter":{"arg0":"java.io.Writer"}},{"notify":{}},{"notifyAll":{}},{"serializeNulls":{}},{"toJson":{"arg0":"com.google.gson.JsonElement"}},{"toJson":{"arg0":"java.lang.Object"}},{"toJson":{"arg0":"com.google.gson.JsonElement","arg1":"com.google.gson.stream.JsonWriter"}},{"toJson":{"arg0":"com.google.gson.JsonElement","arg1":"java.lang.Appendable"}},{"toJson":{"arg0":"java.lang.Object","arg1":"java.lang.Appendable"}},{"toJson":{"arg0":"java.lang.Object","arg1":"java.lang.reflect.Type"}},{"toJson":{"arg0":"java.lang.Object","arg1":"java.lang.reflect.Type","arg2":"com.google.gson.stream.JsonWriter"}},{"toJson":{"arg0":"java.lang.Object","arg1":"java.lang.reflect.Type","arg2":"java.lang.Appendable"}},{"toJsonTree":{"arg0":"java.lang.Object"}},{"toJsonTree":{"arg0":"java.lang.Object","arg1":"java.lang.reflect.Type"}},{"toString":{}},{"wait":{}},{"wait":{"arg0":"long"}},{"wait":{"arg0":"long","arg1":"int"}}]}
    
  3. 实现异步回调接口的 callback. SDK 中可能含有大量的异步回调接口,回调的 callback 可能有很多的中间状态,框架没有办法判断触发了哪一个判断代表了本次接口调用完毕,所以如果是异步接口的话,回调需要用户自己实现.回调在 utils/CallBackImpl.java中实现,在utils/Init.java文件中的 initCallback 方法中注册.

// callback 的定义
public static Callback callback = new Callback() {
        @Override
        public void onSuccess(String t) {
            // 成功,只需要设置返回值
            responseMap.put("token", t);
            test_done = true;
        }

        @Override
        public void onError(ErrorCode e) {
            // 失败,需要设置失败的 code
            responseMap.put(AshenConst.errorCode, e.getValue());
            responseMap.put(AshenConst.code, AshenConst.ERROR);
            test_done = true;
        }

        @Override
        public void onProgress(Code code) {
            // 中间状态,可以什么都不做,或者给结果的 map 中自定义设置任何属性
        }
    };

开始测试

使用 HTTP 调用 Gson,假设我们测试 Gson 的 equals 接口,我们使用浏览器访问

http://192.168.1.100:9999/getInterfaceParams?name=equals
浏览器返回了参数数量,和每个参数的类型

{"message":[{"com.google.gson.Gson":{"equals":{"arg0":"java.lang.Object"}}}]}

使用 Python 测试这个接口

import requests

r = requests.post("http://192.168.1.100:9999/equals",json={"arg0":{"a":"b"}})
print(r.text)

{"code":200,"message":{"message":false},"type":"success","time_diff(ms)":"0"}

返回数据中的 {"message":false} 就是实际接口的返回值

说明

  1. demo 默认的端口设置的为 9999,这个是可以在源码中修改的.
  2. 启动的为 HTTP 服务,目前只内置了两个接口,一个是 /getAllInterface 获取所有接口,一个是 /getInterfaceParams?name=methodName 获取被测接口
  3. 本质上是一个 HTTP server,所以可以二次开发给 app 增加更多的接口,也可以直接返回 html,便于用户操作
  4. 如果不同的类具有同名同参的接口,则请求增加 testClassName 的 key,value 写类名可区分,如果是不同包下的类名也一致,则可以连包名都写上,是一个 contains 的概念,如果匹配到多个则默认使用第一个
  5. 对于 json 转对象使用的是 Gson 库,如果默认转的不符合需求,可以注册 JsonDeserializer,项目中默认增加了对于 android.net.Uri 的 decode 和 encode
  6. 项目中关于 HTTP 请求处理的主文件是 src/main/java/com/android/AshenAndroid/server/impl/AshenHTTPServlet.java
  7. 需要更多内置文件:将文件添加到 src/main/assets 目录中,再配置到 AshenConst.java 的 fileNameList 中
  8. 对于服务端主动推送的消息,可以写一个接口,把消息存到 list 中,需要的时候就返回
  9. 识别 callback 的参数使用的是参数名包含 "callback",参数如果为 callback 类型时,参数状态为可选,不写的话默认去 callback 的注册 map 中寻找,如果有重载接口且参数列表只有 callback 类型不一样,则需要在参数列表中写上 callback.
  10. 如果要测试弱网,则可以使用 adb 将设备的端口转发到电脑上,这样访问电脑端口测试处在弱网环境的手机
共收到 27 条回复 时间 点赞
小抄 在 AsheniOS — iOS SDK 接口测试 中提及了此贴 03月26日 16:30

感觉挺牛,先收藏下,看看以后有没有机会学习

// 只需给 classRegistered put 就可以,key 为包路径,value 为调用 api 的 SDK 实例,单例类为 instance,例如 Gson
楼主请教下,key 这个包名指的是打成 apk 的包名嘛?

对 刺猬Hedgehog 回复

是的

demo 貌似运行不起来,找不到方法。

对 刺猬Hedgehog 回复

这个测试过的哈,空 demo 妥妥的可以运行,你检查下是不是 gradle 下载不下库或者是怎样,先试试空 demo 运行,再集成 SDK 呢? 而且你也没说报错或者没有提供任何信息.这个是没有办法排查的

对 刺猬Hedgehog 回复

楼主请教下,key 这个包名指的是打成 apk 的包名嘛?
不好意思,这个问题没有看清,这个不是 apk 的包名,是类的路径,也就是你在其他类 import 这个类的时候 import 后边写的那个字符串.

对 小抄 回复
03-30 10:55:10.184 8083-8083/com.android.im_lib_test E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.android.im_lib_test, PID: 8083
    java.lang.NoSuchMethodError: No virtual method getParameters()[Ljava/lang/reflect/Parameter; in class Ljava/lang/reflect/Method; or its super classes (declaration of 'java.lang.reflect.Method' appears in /system/framework/core-libart.jar)
        at com.android.AshenAndroid.utils.ClassUtil.getClassObjectNotCallBack(ClassUtil.java:27)
        at com.android.AshenAndroid.utils.Init.initClassRegister(Init.java:88)
        at com.android.AshenAndroid.utils.Init.<init>(Init.java:40)
        at com.android.AshenAndroid.MainActivity.onCreate(MainActivity.java:83)
        at android.app.Activity.performCreate(Activity.java:6497)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1108)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2455)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2562)
        at android.app.ActivityThread.access$1100(ActivityThread.java:165)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1430)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:179)
        at android.app.ActivityThread.main(ActivityThread.java:5672)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:674)

重新从 git 上下载代码运行了下空 demo,报找不到方法错误。

https://github.com/Kotlin/kotlinx.serialization/issues/1010 我查到 Kotlin 有说这个 bug,但是这个项目不是 Kotlin,你有修改什么配置文件吗,比如 android 版本?

对 小抄 回复

找到原因了,android api 版本问题,我的手机是 android6.0 系统的,修改了 minSdkVersion 为 21。改回 26 再换部手机就可以运行了。ps:“// Required when setting minSdkVersion to 20 or lower” 这句话有迷惑性😂

我再试试通过 getGenericParameterTypes 获取类型,然后再获取真实类型,貌似这个没有 api 版本的要求。

小抄 #12 · 2021年03月30日 Author

👍 ,建议被测的 SDK 是 jdk 1.8 打包的,并且增加了 -parameters 的参数,这样就可以拿到入参的形参名

对 小抄 回复

好咧,多谢楼主~

思路可以啊 😀

小抄 #15 · 2021年04月02日 Author

😄还是难免有弊端,sdk 崩溃就直接凉凉,通讯就断了

对 小抄 回复

目前落地效果咋样

好巧,我们也是最近在实践 SDK 的测试框架,一样的原理,IOS 那边是扒的 WDA 的逻辑实现,这些调用其实还好,主要我们还要封装测试逻辑代码,代码能力不强,好尴尬

小抄 #18 · 2021年04月06日 Author

目前做接口测试和日常的功能回归,大部分的功能是可以覆盖的,也有不能主动触发的比如服务端主动推送的功能需要定制的测试一下,对人力的节省和质量的提升还是相对显著的

小抄 #19 · 2021年04月06日 Author
对 剪烛 回复

最开始的设计是 http 传入的参数再用硬编码实现一遍传入对应的接口中,但是我们版本很多,要根据不同的 SDK 版本维护很多分支,等于是一个测试对应多个开发,demo 就维护的很累了,所以使用反射来调用,这样就不需要维护 demo 了,现在不用写逻辑代码,只需要使用 http 把逻辑串起来就好了,感觉现在就像是做普通的 http server 测试一样

对 小抄 回复

哈哈哈,可以在接口调用层搞个判断,出现崩溃,记录一下,然后重新调起

小抄 #21 · 2021年04月08日 Author
对 剪烛 回复

👍 我得看看 Android 怎么全局捕获崩溃

您好,请教一下,您这个被测试的 SDK,是不是要求格式是.jar 的形式?对于 SDK 被打包成.aar 的格式的支持吗?

小抄 #23 · 2021年04月19日 Author
对 爱吃螃蟹 回复

可以的,只要以 JDK1.8 打包且增加了 -parameter 参数的都可以

我也做了一个

用 jmeter 的方式测试代码😁

小抄 #26 · 2021年04月27日 Author
对 甬力君 回复

这种脱离了真机的话,比如推送/摄像头什么的功能调用怎么测试呀


我 demo 运行起来了,但是怎么调用不通呢,浏览器里面请求提示网络不通呢

对 剪烛 回复

代码能力不强 +1,不知道这个具体要怎么用才能测我们自己的 SDK 呢

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册。