9
Android UI自动化测试技术选择与踩坑
source link: https://www.viseator.com/2018/02/09/android_ui_test/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在官方文档关于测试一节中,介绍了测试金字塔这一概念:
即我们应该包括三个层次的测试:小型、中型、与大型:- 小型测试是单元测试,即可以独立运行在每个模块上的测试。它们通常模拟了每个主要的组件并且应该快速地运行。
- 中型测试是介于大型与小型测试之间的综合测试,它们集成了数个组件并且运行在模拟器或实机上。
- 大型测试是以模拟UI工作流方式进行的综合UI测试,它们保证了关键的终端用户的使用可以符合我们的预期。
虽然小型测试迅速并且专注,可以让我们很快地发现错误,但它们同样是低仿真且自成一体的,这使得我们很难保证通过了所有的单元测试就可以成功地让App运行。而大型测试的优缺点恰恰与上述相反。
由于每个层次的测试的角色各不相同,我们应该进行所有这三个层次的测试,尽管各个层次使用比例需要根据App的使用特点,通常建议三个测试的比例为1:2:7。
UI自动化测试即属于上面说的大型测试。测试框架功能对比
参考: - https://stackoverflow.com/questions/20046021/google-espresso-or-robotium
实际测试编写体验
实际的编写中,主要的步骤可以总结为三步:
- 如何定位想要操作的
View
- 如何施加想要进行操作
- 如何判断App的行为符合我们预期
三种框架都为我们提供了一系列方法,但细节与效果略有不同:
- Espresso
- 白盒测试,体现在可以直接拿到显示中的
View
实例,拿到WebView
DOM
树中的Element
- 一般场景下,区分度较为明显的
View
(有唯一的id
tag
)等,可以通过多种途径定位,较为便捷 - 面对特殊场景:如
TabLayout
中的Tab
时,由于它们拥有相同的类型与id
,难以定位view
- 出现多窗口情况(如
dialog
),可以正常处理 - 不能触发按返回键、改变屏幕方向等操作
- 白盒测试,体现在可以直接拿到显示中的
- UI Automator
- 黑盒测试,体现在无法拿到具体的
View
,只能拿到基类(LinearLayout
等),无法看到WebView
的DOM
树 - 一般场景下,定位
View
没有差别 - 面对特殊场景,可以通过找出所有符合条件的
View
再按索引找到想要的View
- 出现多窗口情况:处理出现异常
- 黑盒测试,体现在无法拿到具体的
- Robotium
- 集合了上述框架的优点,既可以拿到显示中的
View
实例与WebView
的DOM
树 - 对上述框架的接口进行了统一,调用比较方便
最终框架选择
通过上述比较,可以看到Robotium在满足我们要求的同时统一了接口,故选择Robotium作为使用的框架。使用过程中踩的一些大坑
变量必须使用static
在AndroidTest
类中,期望使用一个boolean
标志来判断是否已经登陆过(避免重复检查登陆状态),发现在login()
方法中置标志为true后进入下一个测试时这个值仍为false
,推测运行测试方法时各个方法的运行是独立的,故不使用静态变量则无法保存状态。等待引发的问题
在我们对View
进行一个操作以后,框架会自动处理下一步动作触发的时机,比如点击一个Tab
后,会自动等待下一个页面出现再执行下面的操作。这个等待判断的原理没有看过源码不能确定,但是实际中遇到比如WebView
加载页这样等待时间较长的页面,就会触发下一个操作的执行。
那么问题就出现了,如果想要进行这样的测试:点击打开一个文档,等待文档打开完毕以后检查标题是否是我们打开的文档,如果在文档没有加载完的时候就执行检查步骤,就会产生Element not found
的错误。
- 集合了上述框架的优点,既可以拿到显示中的
- 强行设置等待时间
利用SystemClock.sleep()
方法强行让测试暂停一段时间,这个方法比较暴力也不优雅,不到万不得已不要使用。 - 使用
Robotium
提供的各种wait
方法,通过设置退出条件来等待:在解决上面的问题的时候就使用了上面的代码来等待1
2
3
4
5
6
7
8
9
10
11
12
13
14private void waitForWebView() {
assertTrue(mSolo.waitForCondition(new Condition() {
@Override
public boolean isSatisfied() {
View loading = null;
try {
loading = mSolo.getView(R.id.loading);
} catch (AssertionFailedError e) {
Log.e(TAG, e.getMessage());
}
return null == loading || View.GONE == loading.getVisibility();
}
}, DOC_LOAD_TIMEOUT));
}WebView
文档加载完毕,返回true
时条件满足,退出等待,若超时,则方法返回false
,assert
失败表示doc
加载超时。此处的判断方法是等待loading
隐藏。上述代码是为了等待1
2
3
4
5
6private void waitForActivity(Class<? extends Activity> activity) {
if (mSolo.getCurrentActivity().getClass() == activity) {
return;
}
assertTrue(mSolo.waitForActivity(activity));
}activity
启动,可以用于判断新的activity
是否正常启动。跨进程引发的问题
在应用中,打开的文档运行在一个新的进程中,在使用Espresso
的时候就遇到了问题:无法拿到新进程中WebView
的信息,原因没有仔细分析,但可以确定是跨进程的问题。在Robotium
中这个问题同样存在。
不但如此,多进程还会导致当前activity
的判断出错,本应判断在DocActivity
中,但实际上得到的是在原进程的activity
中。
在多种方法尝试无果后,只能暂时修改源码,将doc
放在同进程打开。View获取的问题
获取想要的View
是编写用例最主要的难点所在,在获取View
的时候也遇到了不少的坑:重复出现的View
实际上通过getCurrentViews()
获取到的View对象包括所有activity
的所有View
,比如主页面有3个tab
,每个tab
中有一个RecyclerView
,这三个RecyclerView都是可以被获取到的(而不是想象中的只获取到当前可见的这个),甚至在打开新的activity
后,后台的activity
中的RecyclerView
还是可以被获取到的。但是使用getView()
方法获取的范围是当前activity
。
这意味着什么呢?如果这些RecyclerView
有相同的id
,使用getView(int id)
方法获取到的只是第一个,即使切到了第二个tab
,获取到的还是第一个tab
中的RecyclerView
。
面对这个情况我们可以用三种方法: - 如果它们
id
不同,使用getView(int id)
就可以拿到特定的。如果id
相同,可以传入第二个index
参数来获取同id
的第n个实例 - 使用
getView(类名, int index)
拿到该类所有实例中的第n个,因为各个RecyclerView
加载的顺序是相对固定的,所以每次运行拿到的RecyclerView
是同一个。拿上面的例子来说,如果要拿到第二个tab
中的RecyclerView
,要获取的应该是第2个。 - 先获取它的任意一个
ParentView
,然后通过getCurrentViews(类名,ViewGroup)
方法拿到List
,如果ViewGroup
是唯一的,这个List
中应该只会有我们想要的那个,也可以用ViewGroup
来缩小我们搜索的范围。View获取的技巧
总结一下 - 定位
View
最为方便的就是使用getView(int id/类名)
这个方式,如果id
/类名的实例唯一,就可以直接拿到。 - 如果同
id
/类名有很多个view
存在,要使用getView(int id/类名, int index)
,拿到第n个view
实例。 - 如果该
view
所处的任一个ViewGroup
很好获取(有唯一id/类名),可以通过getCurrentViews(类名, ViewGroup)
这个方式迅速缩小范围,拿到想要的View
。RecyclerView中获取ViewHolder
Robotium
允许我们直接拿到View
对象,所以我们可以直接使用RecyclerView
的findViewHolderForAdapterPosition()
方法去拿ViewHolder
,但是事情并没有这么简单,我们知道RecyclerView
的特点是没有在屏幕上显示的ViewHolder
是没有被实例化的,这样拿到的会是null
,所以为了拿到所有ViewHolder
我们还需要使用scrollDownRecyclerView()
方法让RecyclerView
滚动起来,但是使用这个方法还会有问题,有时候它会失效(怀疑是没有完成滚动就执行了下一条语句),所以还需要在调用这个方法之后设一个延迟(100ms就行),样例代码如下:1
2
3
4
5
6
7
8
9
10
11for (int i = 0; i < listAdapter.getItemCount(); i++) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i);
if (null == viewHolder) {
mSolo.scrollDownRecyclerView(0);
SystemClock.sleep(100);
viewHolder = recyclerView.findViewHolderForAdapterPosition(i);
}
if (testAction.test(listAdapter, viewHolder, i)) {
break;
}
}WebView中的WebElement获取需要延迟
之前介绍了等待WebView
加载的方法,但是实际上这个方法返回后通过getWebElements()
拿到的WebElements
是空的,实际上想要拿到WebElement
还要等待几秒种的时间。输入字符的问题
直接输入字符的方法
会把1
2
3private void inputString(String text) {
InstrumentationRegistry.getInstrumentation().sendStringSync(text);
}string
拆分成按键序列输入。丢失字符的问题
调用上面的方法输入的字符过长的时候会偶发出现字符丢失的问题,暂时不知道解决方法,只能输入短一点的字符。方法调用顺序的问题
写在AndroidTest
文件夹下同一个测试类中的各个方法的调用顺序是未知的,而且没有找到好的办法可以直接在内部决定它们的调用顺序,本来这并不是一个大问题,但是在写测试的过程中出现了一个比较致命的问题:连续打开WebView
会导致WebView
无法加载。这个问题应该是由于复用了WebView
或者loading view
的判断出了问题导致的。单独单次地运行单个测试方法并不会出现这个问题,所以考虑转而使用手写测试脚本 的方式来决定方法的调用顺序并且单次地运行单个测试方法。同时脚本还应该支持重新编译测试Apk并且安装到手机,并且可以指定测试的方法与执行顺序。
最终编写完成的脚本如下:需要测试的方法按顺序放在funtion_names.txt文件中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#!/bin/bash
function rebuild_install() {
./gradlew --build-cache :app:assemblePublishxxxDebug :app:assemblePublishxxxDebugAndroidTest
adb push app/build/outputs/apk/publishxxx/debug/app-publish-xxx-debug.apk /data/local/tmp/com.xxx.xx.xxxx
adb shell pm install -t -r "/data/local/tmp/com.xxx.xx.xxxx"
adb push app/build/outputs/apk/androidTest/publishxxx/debug/app-publish-xxx-debug-androidTest.apk /data/local/tmp/com.xxx.xx.xxxx
adb shell pm install -t -r "/data/local/tmp/com.xxx.xx.xxxx"
}
cd ..
if [ -n "$1" ] && [ "$1" = rebuild ]
then
rebuild_install
else
echo Not Rebuild
fi
test_funcs=($(awk '{print $0}' ui_test/funtion_names.txt))
for funcs in ${test_funcs[@]}
do
echo ┌--------------------------------------
echo Start $funcs
adb shell am force-stop com.xxx.xx.xxxx
adb shell am instrument -w -r -e debug false -e class com.xxx.xx.xxxx.MainInstrumentedTest#$funcs com.xxx.xx.xxxx.test/android.support.test.runner.AndroidJUnitRunner | sed -En -e '/There was/,/FAILURES/p;/OK/p'
echo └--------------------------------------
done在使用时可以传入1
2
3
4checkLogout
checkNewDoc
checkCreateDoc
checkOpenSharerebuild
参数来重新构建并安装,未传入可以直接开始测试:运行结果示例:1
2./test.sh rebuild // 重新构建并安装
./test.sh //直接测试1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Not Rebuild
┌--------------------------------------
Start checkLogout
OK (1 test)
└--------------------------------------
┌--------------------------------------
Start checkNewDoc
OK (1 test)
└--------------------------------------
┌--------------------------------------
Start checkCreateDoc
OK (1 test)
└--------------------------------------
┌--------------------------------------
Start checkOpenShare
OK (1 test)
└--------------------------------------
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK