WeTest导读
说起Android的自动化测试,相信有很多小伙伴都接触过或者有所耳闻,本文从框架最基本的功能介绍及API的使用入手,结合简单的项目实战来帮忙大家对该框架进一步理解和加深印象。下面让我们来一睹标准App的四大自动化测试法宝的风采!
法宝1:稳定性测试利器——Monkey
要想发布一个新版本,得先通过稳定性测试。理想情况是找个上幼儿园的弟弟妹妹,打开应用把手机交给他,让他胡乱的玩,看你的程序能不能接受这样的折腾。但是我们身边不可能都有正太和萝莉,也不能保证他们拿到手机后不是测试软件的健壮性,反而测试你的手机经不经摔,这与我们的期望差太远了…
Google公司考虑到我们的需要,开发出了Monkey这个工具。但在很多人的印象中,Monkey测试就是让设备随机的乱点,事件都是随机产生的,不带任何人的主观性。很少有人知道,其实Monkey也可以用来做简单的自动化测试工作。
Mokey基本功能介绍
首先,介绍下Monkey的基本使用,如果要发送500个随机事件,只需运行如下命令:adb shell monkey 500
插上手机运行后,大家是不是发现手机开始疯狂的运行起来了。So Easy!
在感受完Monkey的效果后,发现这“悟空”太调皮了,根本招架不住啊!是否有类似“紧箍咒”这种约束类命令,让这只猴子在某个包或类中运行呢?要想Monkey牢牢的限制在某个包中,命令也很简单:adb shell monkey –p your-package-name 500
-p后面接你程序的包名。多想限制在多个包中,可以在命令行中添加多个包:
adb shell monkey –p your-package1-name –p your-package2-name 500
这样“悟空”就飞不出你的五指山了。
Mokey编写自动化测试脚本
若控制不住“悟空”,只让它随机乱点的话,Monkey是替代不了黑盒测试用例的。我们能不能想些办法,控制住“悟空”让他做些简单的自动化测试的工作呢?下面来看一下,如何用Monkey来编写脚本。
先简单介绍下Monkey的API,若有需要详细了解的小伙伴,可自行百度或谷歌一下查阅哈。
(1) 轨迹球事件:DispatchTrackball(参数1~参数12)
(2) 输入字符串事件:DispatchString(String text)
(3) 点击事件:DispatchPointer(参数1~参数12)
(4) 启动应用:LaunchActivity(String pkg_name, String class_name)
(5) 等待事件:UserWait(long sleeptime)
(6) 按下键值:DispatchPress(int keyCode)
(7) 长按键值:LongPress(int keyCode)
(8) 发送键值:DispatchKey(参数1~参数8)
(9) 打开软键盘:DispatchFlip(Boolean keyboardOpen)
了解完常用API后,我们来看一下Monkey脚本的编写规范。Monkey Script是按照一定的语法规则编写的有序的用户事件流,使用于Monkey命令工具的脚本。Monkey脚本一般以如下4条语句开头:
1
2
3
4
5
|
<code><code><code>#
type
count
10
speed
1.0
start
|
下面来看一个简单应用的实战,实现的效果很简单,就是随便输入文本,选择选项再进行提交,提交后要验证提交后的效果。
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
27
|
<code><code><code>#
Script
type
count
10
speed
1.0
start
#
1
captureDispatchPointer(
10
,
10
,
0
,
210
,
200
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
captureDispatchPointer(
10
,
10
,
1
,
210
,
200
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
#
1
内容
captureDispatchString(Hello)
#
2
captureDispatchPointer(
10
,
10
,
0
,
210
,
280
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
captureDispatchPointer(
10
,
10
,
1
,
210
,
280
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
#
2
内容
captureDispatchString(Ringo)
#
captureDispatchPointer(
10
,
10
,
0
,
210
,
420
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
captureDispatchPointer(
10
,
10
,
1
,
210
,
420
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
#
captureDispatchPointer(
10
,
10
,
0
,
338
,
476
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
captureDispatchPointer(
10
,
10
,
1
,
338
,
476
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
#
500
毫秒
UserWait(
500
)
#
captureDispatchPointer(
10
,
10
,
0
,
100
,
540
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)
captureDispatchPointer(
10
,
10
,
1
,
100
,
540
,
1
,
1
,-
1
,
1
,
1
,
0
,
0
)</code></code></code>
|
将上述代码另存为HelloMonkey文件,然后将该脚本推送到手机的sd卡里。
1
|
<code><code><code>adb
|
然后运行:
1
|
<code><code><code>adb
1
</code></code></code>
|
脚本后面的数字1表示运行该脚本的次数。小伙伴们可以安装附件里的Bugben.apk再执行下脚本感受下哦!
Monkey工具总结
Monkey可以编写脚本做简单的自动化测试,但局限性非常大,例如无法进行截屏操作,不能简单的支持插件的编写,没有好的办法控制事件流,不支持录制回放等。我们在平时的使用中,关注较多的是利用好Monkey的优势,如不需源码,不需编译就可以直接运行。
法宝2:Monkey之子——MonkeyRunner
Monkey虽然能实现部分的自动化测试任务,但本身有很多的局限性,例如不支持截屏,点击事件是基于坐标的,不支持录制回放等。我们在实际应用中,尽量关注利用好Monkey测试的优势。若平时的工作中遇到Monkey工具无法满足的,这里给大家推荐另一款工具MonkeyRunner。
同样先简单的介绍下MonkeyRunner的API,这里重点介绍能够实现上文Monkey脚本的API,其余的API感兴趣的小伙伴可以自行查阅。
(1) 等待设备连接:waitForConnection()
(2) 安装apk应用:installPackage(String path)
(3) 启动应用:startActivity(String packageName+activityName)
(4) 点击事件:touch(int xPos, int yPos, dictionary type)
(5) 输入事件:type(String text)
(6) 等待:sleep(int second)
(7) 截图:takeSnapshot()
(8) 发送键值:press(String name, dictionary type)
MokeyRunner编写自动化测试脚本
下面我们来看下,用MonkeyRunner实现的自动化脚本。
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
<code><code><code>
#
import
from
import
#
txt1_x
210
txt1_y
200
txt2_x
210
txt2_y
280
txt3_x
210
txt3_y
420
txt4_x
338
txt4_y
476
submit_x
100
submit_y
540
type
'DOWN_AND_UP'
seconds
1
txt1_msg
'Hello'
txt2_msg
'MonkeyRunner'
#
package
package
'com.ringo.bugben'
activity
'.MainActivity'
component
package
'/'
+activity
#
device
#
device.installPackage(
'./bugben.apk'
)
print
'Install bugben.apk...'
#
print
'Launching bugben...'
#
MonkeyRunner.sleep(seconds)
#
device.touch(txt1_x,
print
'Inputing txt1...'
#
device.touch(txt2_x,
device.type(txt2_msg)
print
'Inputing txt2...'
#select
device.touch(txt3_x,
device.touch(txt4_x,
#
MonkeyRunner.sleep(seconds)
#
print
'Submiting...'
#
MonkeyRunner.sleep(seconds)
#
picture
picture.writeToFile(
'./HelloMonkeyRunner.png'
,
'png'
)
print
'Complete! See bugben_pic.png in currrent folder!'
#
device.press(
'KEYCODE_HOME'
, type)
print
'back to home.'
</code></code></code>
|
将脚本保存为HelloMonkeyRunner.py,并和Bugben.apk一起拷贝到Android SDK的tools目录下,执行monkeyrunner HelloMonkeyRunner.py
执行完成后,效果如上,并且会在当前目录生成HelloMonkeyRunner.png截图。
MokeyRunner的录制回放
首先是环境配置,在源码“~\sdk\monkeyrunner\scripts”目录下有monkey_recZ喎�"/kf/ware/vc/" target="_blank" class="keylink">vcmRlci5webrNbW9ua2V5X3BsYXliYWNrLnB5o6y9q9Xiwb249s7EvP4ouL28/tbQ09DV4sG9zsS8/im/vbG0tb1TREu1xHRvb2xzxL/CvM/Co6y+zb/J0tTNqLn9yOfPwrT6wuu9+NDQxvS2r6O6PC9jb2RlPjwvY29kZT48L2NvZGU+PC9wPg0KPHByZSBjbGFzcz0="brush:java;">monkeyrunner
monkey_recorder.py
运行结果如下图所示:
下面用MonkeyRecorder提供的控件,来进行脚本的录制。
录制完成后,导出脚本保存为HelloMonkeyRunnerRecorder.mr,用文本编辑器打开代码如下:
1
2
3
4
5
6
|
<code><code><code>TOUCH|{
'x'
:
317
,
'y'
:
242
,
'type'
:
'downAndUp'
,}
TYPE|{
'message'
:
'Hello'
,}TOUCH|{
'x'
:
283
,
'y'
:
304
,
'type'
:
'downAndUp'
,}
TYPE|{
'message'
:
'MonkeyRecorder'
,}
TOUCH|{
'x'
:
249
,
'y'
:
488
,
'type'
:
'downAndUp'
,}
TOUCH|{
'x'
:
375
,
'y'
:
544
,
'type'
:
'downAndUp'
,}
TOUCH|{
'x'
:
364
,
'y'
:
626
,
'type'
:
'downAndUp'
,}</code></code></code>
|
脚本录制完毕,接来下看看回放脚本是否正常。回放脚本时执行以下命令:
monkeyrunner monkey_playback your_script.mr
由于脚本中未加入拉起应用的代码,这里运行前需手动拉起应用。
结果运行正常,符合我们的预期。
MonkeyRunner工具总结
MonkeyRunner有很多强大并好用的API,并且支持录制回放和截图操作。同样它也不需源码,不需编译就可以直接运行。但MonkeyRunner和Monkey类似,也是基于控件坐标进行定位的,这样的定位方式极易导致回放失败。
法宝3:单元测试框架——Instrumentation
Monkey父子均可通过编写相应的脚本,在不依赖源码的前提下完成部分自动化测试的工作。但它们都是依靠控件坐标进行定位的,在实际项目中,控件坐标往往是最不稳定的,随时都有可能因为程序员对控件位置的调整而导致脚本运行失败。怎样可以不依赖坐标来进行应用的自动化测试呢?下面就要亮出自动化测试的屠龙宝刀了——Instrumentation框架。
Instrumentation框架主要是依靠控件的ID来进行定位的,拥有成熟的用例管理系统,是Android主推的白盒测试框架。若想对项目进行深入的、系统的单元测试,基本上都离不开Instrumentation这把屠龙宝刀。
在了解Instrumentation框架之前,先对Android组件生命周期对应的回调函数做个说明:
从上图可以看出,Activity处于不同状态时,将调用不同的回调函数。但Android API不提供直接调用这些回调函数的方法,在Instrumentation中则可以这样做。Instrumentation类通过“hooks”控制着Android组件的正常生命周期,同时控制Android系统加载应用程序。通过Instrumentation类我们可以在测试代码中调用这些回调函数,就像在调试该控件一样一步一步地进入到该控件的整个生命周期中。
Instrumentation和Activity有点类似,只不过Activity是需要一个界面的,而Instrumentation并不是这样的,我们可以将它理解为一种没有图形界面的,具有启动能力的,用于监控其他类(用Target Package声明)的工具类。
下面通过一个简单的例子来讲解Instrumentation的基本测试方法。
1. 首先建立项目名为HelloBugben的Project,类名为HelloBugbenActivity,代码如下:
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
27
28
29
30
31
32
33
34
35
36
37
38
|
<code><code><code>
package
import
import
import
import
import
public
HelloBugbenActivity
extends
private
private
@Override
protectedvoidonCreate(Bundle
super
.onCreate(savedInstanceState);
setContentView(R.layout.main);
String
"bugben"
;
Boolean
true
;
Float
float
)
60.0
;
textview1
textview2
setTxt(bugben_txt);
setTv1Bold(bugben_bold);
setTv2Size(bugben_size);
}
publicvoidsetTv2Size(Float
//
TextPaint
tp.setTextSize(bugben_size);
}
publicvoidsetTv1Bold(Boolean
//
TextPaint
tpPaint.setFakeBoldText(bugben_bold);
}
publicvoidsetTxt(String
//
textview1.setText(bugben_txt);
textview2.setText(bugben_txt);
}
}</code></code></code>
|
这个程序的功能很简单,就是给2个TextView的内容设置不同的文本格式。
2. 对于测试工程师而言,HelloBugben是一个已完成的项目。接下来需创建一个测试项目,选择“New->Other->Android Test Project”,命名为HelloBugbenTest,选择要测试的目标项目为HelloBugben项目,然后点击Finish即可完成测试项目的创建。
可以注意到,该项目的包名自带了com.example.hellobugben.test这个test标签,这就说明该测试项目是针对HelloBugben所设置的。
打开AndroidManifest可看到标签,该标签元素用来指定要测试的应用程序,自动将com.example.hellobugben设为targetPackage对象,代码清单如下:
1
|
<code><code><code><!--?xml
"1.0"
"utf-8"
?--></code></code></code>
|
在标签中,android:name声明了测试框架,android:targetPackage指定了待测项目包名。
下面来看一下,如何用Instrumentation框架编写测试程序,代码如下:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
<code><code><code>
package
import
import
import
import
import
import
public
public
super
(HelloBugbenActivity.
class
);
}
HelloBugbenActivity
private
null
;
private
private
String
"bugben"
;
Boolean
true
;
Float
float
)
20.0
;
Float
@Override
public
setUp()
throws
super
.setUp();
helloBugben
textView1
textView2
handler
new
@Override
public
throws
super
.tearDown(); }
public
testSetTxt(){
new
public
if
null
) {
handler.post(runnableTxt);
}
}
}.start();
String
assertTrue(cmpTxtString.compareToIgnoreCase(bugben_txt)
0
);
}
public
testSetBold(){
helloBugben.setTv1Bold(bugben_bold);
TextPaint
Boolean
assertTrue(cmpBold);
}
publicvoidtestSetSize(){
helloBugben.setTv2Size(bugben_sizeFloat);
Float
assertTrue(cmpSizeFloat.compareTo(bugben_sizeFloat)
0
);
}
Runnable
new
@Override
publicvoidrun(){
//
helloBugben.setTxt(bugben_txt);
}
};
}</hellobugbenactivity></code></code></code>
|
上述代码中,我们首先引入import android.test.ActivityInstrumentationTestCase2。其次让HelloBugbenTestBase继承自ActivityInstrumentationTestCase2这个类。接着在setUp()方法中通过getActivity()方法获取待测项目的实例,并通过textview1和textview2获取两个TextView控件。最后编写3个测试用例:控制文本设置测试testSetText()、字体加粗属性测试testSetBold、字体大小属性测试testSetSize()。这里用到的关键方法是Instrumentation
API里面的getActivity()方法,待测的Activity在没有调用此方法的时候是不会启动的。
眼尖的小伙伴可能已经发现控制文本设置测试这里启用了一个新线程,这是因为在Android中相关的view和控件不是线程安全的,必须单独在新的线程中做处理,不然会报
1
2
|
<code><code><code>android.view.ViewRootImpl$CalledFromWrongThreadException:
Only
|
这个错误。所以需要启动新线程进行处理,具体步骤如下:
1) 在setUp()方法中创建Handler对象,代码如下:
1
2
3
4
|
<code><code><code>
public
setUp()
throws
super
.setUp();
handler =
new
}</code></code></code>
|
2) 创建Runnable对象,在Runnable中进行控件文本设置,代码如下:
1
2
3
4
5
6
7
|
<code><code><code>
new
@Override
public
run(){
//
helloBugben.setTxt(bugben_txt);
}
};</code></code></code>
|
3) 在具体测试方法中通过调用runnable对象,实现文本设置,代码如下:
1
2
3
4
5
6
7
|
<code><code><code>
new
public
run() {
if
null
) {
handler.post(runnableTxt);
}
}
}.start();
|
我们运行一下结果,结果截图如下:
可以看到3个测试用例结果运行正常。
可能有小伙伴要问,程序中为啥要继承ActivityInstrumentationTestCase2呢?我们先看一下ActivityInstrumentationTestCase2的继承结构:
java.lang.Object
junit.framework.Assert
junit.framework.TestCase
android.test.InstrumentationTestCase
android.test.ActivityTestCase
android.test.ActivityInstrumentationTestCase2
ActivityInstrumentationTestCase2允许InstrumentationTestCase. launchActivity来启动被测试的Activity。而且ActivityInstrumentationTestCase2还支持在新的UI线程中运行测试方法,能注入Intent对象到被测试的Activity中,这样一来,我们就能直接操作被测试的Activity了。正因为ActivityInstrumentationTestCase2有如此出众的有点,它才成功取代了比它早出世的哥哥:ActivityInstrumentationTestCase,成为了Instrumentation测试的基础。
Instrumentation测试框架实战
了解完Instrumentation的基本测试方法后,我们来看一下如何运用Instrumentation框架完成前文Monkey父子完成的自动化测试任务。
首先建立项目名为Bugben的Project,类名为MainActivity,代码如下:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
<code><code><code>
package
import
import
import
import
import
import
import
import
import
import
public
private
null
;
private
null
;
private
null
;
private
null
;
private
null
;
@Override
protected
onCreate(Bundle savedInstanceState){
super
.onCreate(savedInstanceState);
setContentView(R.layout.main);
editText1
editText2
button
bold
small
button.setOnClickListener(
new
@Override
publicvoidonClick(View
Log.v(
"Ringo"
,
"Press Button"
);
String
"bold"
"notbold"
;
String wordSize = small.isChecked() ?
"small"
"big"
;
//
Intent
new
this
, OtherActivity.
class
);
intent.putExtra(
"text1"
, editText1.getText().toString());
intent.putExtra(
"text2"
, editText2.getText().toString());
intent.putExtra(
"isBold"
, isBold);
intent.putExtra(
"wordSize"
, wordSize);
startActivity(intent);
}
});
}
}</code></code></code>
|
在建立一个名为OtherActivity的类,点击提交按钮后,跳转到这个界面,代码如下:
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
27
28
29
30
31
32
33
34
35
36
37
|
<code><code><code>
package
import
import
import
import
import
import
public
private
null
;
private
null
;
Boolean
true
;
Boolean
false
;
Float
float
)
20.0
;
Float
float
)
60.0
;
@Override
super
.onCreate(savedInstanceState);
setContentView(R.layout.other);
textView2
textView3
Intent
textView2.setText(data.getStringExtra(
"text1"
));
textView3.setText(data.getStringExtra(
"text2"
));
if
"isBold"
).equalsIgnoreCase(
"bold"
)) {
TextPaint
tPaint.setFakeBoldText(bugben_bold);
}
else
{
TextPaint
}
if
"wordSize"
).equalsIgnoreCase(
"small"
)) {
TextPaint
tPaint.setTextSize(bugben_small_size);
}
else
{
TextPaint
tPaint.setTextSize(bugben_big_size);
}
}
}</code></code></code>
|
3.接下来需创建一个测试项目,命名为BugbenTestBase,选择要测试的目标项目为Bugben项目,然后点击Finish即可完成测试项目的创建。
在com.ringo.bugben.test包中添加BugbenTestBase这个类,类的代码如下:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
<code><code><code>
package
import
import
import
import
import
import
import
import
import
import
import
import
import
public
BugbenTestBase
extends
publicBugbenTestBase(){
super
(MainActivity.
class
);
}
MainActivity
OtherActivity
private
private
private
private
private
private
private
private
private
//
String
"RingoYan"
;
String
"自动化测试"
;
Boolean
true
;
Boolean
false
;
Float
float
)
20.0
;
Float
float
)
60.0
;
@Override
public
setUp()
throws
super
.setUp();
//
Intent
new
intent.setClassName(
"com.ringo.bugben"
, MainActivity.
class
.getName());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mainActivity
//
txt1
txt2
bold
notbold
small
big
subButton
}
@Override
publicvoidtearDown()
throws
super
.tearDown();
}
//
public
testSubmit()
throws
Log.v(
"Ringo"
,
"test normal submit"
);
//
ActivityMonitor
OtherActivity.
class
.getName(),
null
,
false
);
//
runTestOnUiThread(
new
@Override
publicvoidrun(){
//
txt1.setText(bugben_txt1);
txt2.setText(bugben_txt2);
bold.setChecked(
true
);
big.setChecked(
true
);
//
SystemClock.sleep(
500
);
//
subButton.performClick();
}
});
//
otherActivity
//
assertTrue(otherActivity
null
);
textView1
textView2
assertEquals(bugben_txt1,
assertEquals(bugben_txt2,
TextPaint
Boolean
assertTrue(cmpBold);
Float
assertTrue(cmpSize.compareTo(bugben_big_size)
0
);
//
SystemClock.sleep(
5000
);
}
}</mainactivity></code></code></code>
|
上述代码中,共包括自动化测试需要进行的5个步骤,具体如下:
(1) 启动应用:通过Intent对象setClassName()方法设置包名和类名,通过setFlags()方法设置标示,然后通过getInstrumentation()的startActivitySync(intent)来启动应用,进入到主界面。
(2) 编辑控件:在Android中相关的view和控件不是线程安全的,所以必须单独在新的线程中做处理。代码中我们在runTestOnUiThread(new Runnable())中的run()方法中执行的。
(3) 提交结果:点击提交按钮进行结果的提交,由于点击按钮也属于界面操作,所以也需要在runTestOnUiThread这个线程中完成。
(4) 界面跳转:这是Instrumentation自动化测试中最需要注意的一个点,特别是如何确认界面已经发生了跳转。在Instrumentation中可以通过设置Monitor监视器来确认。代码如下:
1
2
|
<code><code><code>ActivityMonitor
OtherActivity.
class
.getName(),
null
,
false
);</code></code></code>
|
然后通过waitForMonitor方法等待界面跳转。
1
|
<code><code><code>otherActivity
|
若返回结果otherActivity对象不为空,说明跳转正常。
(5) 验证显示:跳转后,通过assertEquals()或assertTrue()方法来判断显示的正确性。
我们运行一下结果,结果截图如下:
Instrumentation工具总结
Instrumentation框架的整体运行流程图如下:
Instrumentation是基于源码进行脚本开发的,测试的稳定性好,可移植性高。正因为它是基于源码的,所以需要脚本开发人员对Java语言、Android框架运行机制、Eclipse开发工具都非常熟悉。Instrumentation框架本身不支持多应用的交互,例如测试“通过短信中的号码去拨打电话”这个用例,被测应用将从短信应用界面跳转到拨号应用界面,但Instrumentation没有办法同事控制短信和拨号两个应用,这是因为Android系统自身的安全性限制,禁止多应用的进程间相互访问。
法宝4:终极自动化测试框架——UIAutomator
鉴于Instrumentation框架需要读懂项目源码、脚本开发难度较高并且不支持多应用交互,Android官网亮出了自动化测试的王牌——UIAutomator,并主推这个自动化测试框架。该框架无需项目源码,脚本开发效率高且难度低,并且支持多应用的交互。当UIAutomator面世后,Instrumentation框架回归到了其单元测试框架的本来位置。
下面我们来看一下这个框架是如何运行起来的。首先运行位于Android SDK的tools目录下的uiautomatorviewer.bat,可以看到启动界面。
启动bugben应用后,点击
这个图标来采集手机的界面信息,如下所示:
我们可以看到,用uiautomatorviewer捕捉到的控件非常清晰,很方便元素位置的定位。在UIAutomator框架中,测试程序与待测程序之间是松耦合关系,即完全不需要获取待测程序的控件ID,只需对控件的文本(text)、描述(content-desc)等信息进行识别即可。
在进行实战之前,我们先看一下UIAutomator的API部分,由以下架构图组成。
下面来看下如何利用该框架创建测试工程。
1. 创建BugBenTestUIAuto项目,右键点击项目并选择Properties > Java Build Path
点击Add Library > Junit > Junit3,添加Junit框架。
点击Add External Jar,并导航到Android SDK目录下,选择platforms目录下面的android.jar和UIAutomator.jar两个文件。
2. 设置完成后,可以开始编写项目测试的代码,具体如下:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
<code><code><code>
package
import
import
import
import
import
import
import
public
BugBenTest
extends
public
super
(); }
String
"xiaopangzhu"
;
String
"bugben"
;
String
"/data/local/tmp/displayCheck.png"
;
String
"com.ringo.bugben/.MainActivity"
;
@Override
public
setUp ()
throws
super
.setUp();
//
startApp(testCmp);
private
startApp(String componentName){
StringBuffer
new
sBuffer.append(
"am start -n "
);
sBuffer.append(componentName);
int
1
;
try
Process
ret
}
catch
//
e.printStackTrace();
}
return
}
@Override
public
tearDown()
throws
super
.tearDown();
}
//
public
testSubmitTest()
throws
"Ringo"
,
"test change the textview's txt and size by UIAutomator"
);
//
UiObject
new
new
"Ringoyan"
));
if
(bugben_et1.exists() && bugben_et1.isEnabled()){
bugben_et1.click();
bugben_et1.setText(bugben_txt1);
}
else
{
Log.e(
"Ringo"
,
"can not find bugben_et1"
); }
//
UiObject
new
new
"18888"
));
if
(bugben_et2.exists() && bugben_et2.isEnabled()){
bugben_et2.click();
bugben_et2.setText(bugben_txt2);
}
else
{
Log.e(
"Ringo"
,
"can not find bugben_et2"
);
}
//
if
(bugben_bold.exists() && bugben_bold.isEnabled()){
bugben_bold.click();
}
else
{
Log.e(
"Ringo"
,
"can not find 加粗"
);
}
//
UiObject
new
new
"大号"
));
if
(bugben_big.exists() && bugben_big.isEnabled()){
bugben_big.click();
}
else
{
Log.e(
"Ringo"
,
"can not find 大号"
); }
//
UiObject
new
new
"提交"
));
if
(subButton.exists() && subButton.isEnabled()){
subButton.clickAndWaitForNewWindow();
}
else
{
Log.e(
"Ringo"
,
"can not find 提交"
);}
//
UiObject
new
new
.className(
"android.widget.LinearLayout"
)
.index(
0
)
.childSelector(
new
.className(
"android.widget.FrameLayout"
)
.index(
1
))
.childSelector(
new
.className(
"android.widget.TextView"
)
.instance(
0
)));
//
UiObject
new
new
.className(
"android.widget.LinearLayout"
)
.index(
0
).childSelector(
new
.className(
"android.widget.FrameLayout"
)
.index(
1
))
.childSelector(
new
.className(
"android.widget.TextView"
)
.instance(
1
)));
//
if
assertEquals(bugben_txt1,
}
else
{
Log.e(
"Ringo"
,
"can not find bugben_tv1"
);
}
if
assertEquals(bugben_txt2,
}
else
{
Log.e(
"Ringo"
,
"can not find bugben_tv2"
);
}
//
File
new
Boolean
assertTrue(displayCap);
}
}</code></code></code>
|
上述代码中,我们首先引入import com.android.uiautomator.testrunner.UiAutomatorTestCase类,并让BugbenTest继承自UiAutomatorTestCase这个类。同样,我们来看下UiAutomator框架下自动化测试进行的5个步骤,具体如下:
(1) 启动应用:于Instrumentation框架不同,UiAutomator是通过命令行进行应用启动的。
am start –n 包名/.应用名
(2) 编辑控件:UiAutomator框架中,控件的编辑相对简单,直接通过UiSelector的text()方法找到对应的控件,然后调用控件的setText()即可对其赋值。
1
2
3
4
5
|
<code><code><code>UiObject
new
new
"Ringoyan"
));
if
(bugben_et1.exists() && bugben_et1.isEnabled()){
bugben_et1.click();
bugben_et1.setText(bugben_txt1);
}</code></code></code>
|
(3) 提交结果:点击提交按钮进行结果的提交,也是通过UiSelector的text()方法找到对应的控件,然后调用clickAndWaitForNewWindow()方法来等待跳转完成。
1
2
3
4
|
<code><code><code>UiObject
new
new
"提交"
));
if
(subButton.exists() && subButton.isEnabled()){
subButton.clickAndWaitForNewWindow();
}</code></code></code>
|
(4) 界面跳转元素获取:用uiautomatorviewer捕捉跳转后的控件,例如捕捉跳转后的文本1:
1
2
3
4
5
6
7
8
9
10
|
<code><code><code>UiObject
new
UiObject(
new
.className(
"android.widget.LinearLayout"
)
.index(
0
)
.childSelector(
new
.className(
"android.widget.FrameLayout"
)
.index(
1
))
.childSelector(
new
.className(
"android.widget.TextView"
)
.instance(
0
)));</code></code></code>
|
(5) 验证显示:跳转后,通过assertEquals()或assertTrue()方法来判断显示的正确性。
1
2
|
<code><code><code>
if
{assertEquals(bugben_txt1,
|
至此核心代码部分已编写完毕。UIAutomator有一个麻烦之处:没法通过Eclipse直接编译。可以借助于一系列命令行进行编译,详细步骤如下:
1) 通过如下命令创建编译的build.xml文件
1
|
<code><code><code>android
1
"E:\workspace\BugBenTestUIAuto"
</code></code></code>
|
创建完成后,刷新BugBenTestUIAuto项目,得到如下图:
打开build.xml会看到,编译项目名为BugBenTestUIAuto。
2) 设置SDK的路径:
1
|
<code><code><code>set
"E:\sdk\android-sdk-windows"
</code></code></code>
|
3) 进入测试目录,然后进行编译:
1
|
<code><code><code>cd
|
编译完成后,再次刷新项目,你将看到BugBenTestUIAuto.jar包生成在bin目录下了,如图:
4) 将生成的jar包推送到手机端
1
|
<code><code><code>adb
|
5) 在手机端运行自动化脚本,即jar包中的测试用例,命令行如下:
1
|
<code><code><code>adb
|
运行结果如下,返回OK表示运行成功。
6) 最后,将运行后的截图从手机端拷贝到PC上
1
|
<code><code><code>adb
|
至此整个代码就编译和运行完毕,如果觉得调试时反复修改和编译比较麻烦,可以将以上脚本写成一个批处理文件。
UIAutomator工具总结
相比于Instrumentation工具,UIAutomator工具更灵活一些,它不需要项目源码,拥有可视化的界面和可视化的树状层级列表,极大降低了自动化测试脚本开发的门槛。并且UIAutomator支持多应用的交互,弥补了Instrumentation工具的不足。但UIAutomator难以捕捉到控件的颜色、字体粗细、字号等信息,要验证该类信息的话,需要通过截图的方式进行半自动验证。同时,UIAutomator的调试相比Instrumentation要困难。所以在平时的测试过程中,建议将两者结合起来使用,可达到更佳的效果!