Android标准App的四大自动化测试

2019年12月06日 阅读数:95
这篇文章主要向大家介绍Android标准App的四大自动化测试,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

WeTest导读

提及Android的自动化测试,相信有不少小伙伴都接触过或者有所耳闻,本文从框架最基本的功能介绍及API的使用入手,结合简单的项目实战来帮忙你们对该框架进一步理解和加深印象。下面让咱们来一睹标准App的四大自动化测试法宝的风采!html

法宝1:稳定性测试利器——Monkey

要想发布一个新版本,得先经过稳定性测试。理想状况是找个上幼儿园的弟弟妹妹,打开应用把手机交给他,让他胡乱的玩,看你的程序能不能接受这样的折腾。可是咱们身边不可能都有正太和萝莉,也不能保证他们拿到手机后不是测试软件的健壮性,反而测试你的手机经不经摔,这与咱们的指望差太远了…
Google公司考虑到咱们的须要,开发出了Monkey这个工具。但在不少人的印象中,Monkey测试就是让设备随机的乱点,事件都是随机产生的,不带任何人的主观性。不多有人知道,其实Monkey也能够用来作简单的自动化测试工做。
Mokey基本功能介绍
首先,介绍下Monkey的基本使用,若是要发送500个随机事件,只需运行以下命令:
adb shell monkey 500java

插上手机运行后,你们是否是发现手机开始疯狂的运行起来了。So Easy!
在感觉完Monkey的效果后,发现这“悟空”太调皮了,根本招架不住啊!是否有相似“紧箍咒”这种约束类命令,让这只猴子在某个包或类中运行呢?要想Monkey紧紧的限制在某个包中,命令也很简单:
adb shell monkey –p your-package-name 500
android

-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条语句开头:
web

?
1
2
3
4
5
<code><code><code># Start Script
type = user    #指明脚本类型
count = 10     #脚本执行次数
speed = 1.0    #命令执行速率
start data >>  #用户脚本入口,下面是用户本身编写的脚本</code></code></code>

下面来看一个简单应用的实战,实现的效果很简单,就是随便输入文本,选择选项再进行提交,提交后要验证提交后的效果。
这里写图片描述
这里写图片描述
shell

?
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># Start
Script
type = user
count = 10
speed = 1.0
start data >>LaunchActivity(com.ringo.bugben,com.ringo.bugben.MainActivity)
# 点击文本框 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卡里。windows

?
1
<code><code><code>adb push HelloMonkey /mnt/sdcard/</code></code></code>

而后运行:安全

?
1
<code><code><code>adb shell monkey -v -f /mnt/sdcard/HelloMonkey 1 </code></code></code>

脚本后面的数字1表示运行该脚本的次数。小伙伴们能够安装附件里的Bugben.apk再执行下脚本感觉下哦!架构

Monkey工具总结

Monkey能够编写脚本作简单的自动化测试,但局限性很是大,例如没法进行截屏操做,不能简单的支持插件的编写,没有好的办法控制事件流,不支持录制回放等。咱们在平时的使用中,关注较多的是利用好Monkey的优点,如不需源码,不需编译就能够直接运行。app

法宝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 monkeyrunner modules
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage
# Parameters
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 name and activity name
package = 'com.ringo.bugben'
activity = '.MainActivity'
component = package + '/' +activity
# Connect device
device = MonkeyRunner.waitForConnection()
# Install bugben
device.installPackage( './bugben.apk' )
print 'Install bugben.apk...'
# Launch bugbendevice.startActivity(component)
print 'Launching bugben...'
# Wait 1s
MonkeyRunner.sleep(seconds)
# Input txt1
device.touch(txt1_x, txt1_y, type)device.type(txt1_msg)
print 'Inputing txt1...'
# Input txt2
device.touch(txt2_x, txt2_y, type)
device.type(txt2_msg)
print 'Inputing txt2...'
#select bold and size
device.touch(txt3_x, txt3_y, type)
device.touch(txt4_x, txt4_y, type)
# Wait 1s
MonkeyRunner.sleep(seconds)
# Submitdevice.touch(submit_x, submit_y, type)
print 'Submiting...'
# Wait 1s
MonkeyRunner.sleep(seconds)
# Get the snapshot
picture = device.takeSnapshot()
picture.writeToFile( './HelloMonkeyRunner.png' , 'png' )
print 'Complete! See bugben_pic.png in currrent folder!'
# Back to home
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 com.example.hellobugben;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextPaint;
import android.view.Menu;
import android.widget.TextView;
public class HelloBugbenActivity extends Activity{        
private TextView textview1;        
private TextView textview2;       
@Override       
protectedvoidonCreate(Bundle savedInstanceState){        
super .onCreate(savedInstanceState);          
setContentView(R.layout.main);                   
String bugben_txt = "bugben" ;         
Boolean bugben_bold = true ;        
Float bugben_size = ( float ) 60.0 ;        
textview1 = (TextView)findViewById(R.id.textView1);               
textview2 = (TextView)findViewById(R.id.textView2);               
setTxt(bugben_txt);               
setTv1Bold(bugben_bold);               
setTv2Size(bugben_size);    
  }             
publicvoidsetTv2Size(Float bugben_size){            
// TODO Auto-generated method stub           
TextPaint tp = textview2.getPaint();               
tp.setTextSize(bugben_size);     
  }             
publicvoidsetTv1Bold(Boolean bugben_bold){              
// TODO Auto-generated method stub            
TextPaint tpPaint = textview1.getPaint();                         
tpPaint.setFakeBoldText(bugben_bold);      
}           
publicvoidsetTxt(String bugben_txt){             
// TODO Auto-generated method stub             
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 version= "1.0" encoding= "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 com.example.hellobugben.test;
import com.example.hellobugben.HelloBugbenActivity;
import com.example.hellobugben.R;
import android.os.Handler;
import android.text.TextPaint;
import android.widget.TextView;
import android.test.ActivityInstrumentationTestCase2;
public classHelloBugbenTestBaseextendsActivityInstrumentationTestCase2<hellobugbenactivity>{             
public HelloBugbenTestBase() {        
super (HelloBugbenActivity. class ); 
  }              
HelloBugbenActivity helloBugben;   
private Handler handler = null ;      
private TextView textView1;    
private TextView textView2;         
String bugben_txt = "bugben" ;     
Boolean bugben_bold = true ;     
Float bugben_sizeFloat = ( float ) 20.0 ;     
Float value;         
@Override     
public void setUp() throws Exception{              
super .setUp();          
helloBugben = getActivity();           
textView1 = (TextView)helloBugben.findViewById(R.id.textView1);            
textView2 = (TextView)helloBugben.findViewById(R.id.textView2);            
handler = new Handler();    }           
@Override     
public voidtearDown() throws Exception{             
super .tearDown();      }             
  public void testSetTxt(){        
new Thread(){          
public voidrun(){              
  if (handler != null ) {                                               
handler.post(runnableTxt);                          
             }    
         }             
    }.start();         
String cmpTxtString = textView1.getText().toString();             
assertTrue(cmpTxtString.compareToIgnoreCase(bugben_txt) == 0 );    
   }             
public void testSetBold(){          
helloBugben.setTv1Bold(bugben_bold);        
TextPaint tp = textView1.getPaint();         
Boolean cmpBold = tp.isFakeBoldText();                            
assertTrue(cmpBold);     
  }                 
publicvoidtestSetSize(){            
  helloBugben.setTv2Size(bugben_sizeFloat);            
Float cmpSizeFloat = textView2.getTextSize();                     
assertTrue(cmpSizeFloat.compareTo(bugben_sizeFloat) == 0 );   
    }                
Runnable runnableTxt = new Runnable() {                              
@Override          
publicvoidrun(){               
// TODO Auto-generated method stub           
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 the original thread that created a view hierarchy can touch its views</code></code></code>

这个错误。因此须要启动新线程进行处理,具体步骤以下:
1) 在setUp()方法中建立Handler对象,代码以下:

?
1
2
3
4
<code><code><code> public void setUp() throws Exception{          
  super .setUp();           
  handler = new Handler();
    }</code></code></code>

2) 建立Runnable对象,在Runnable中进行控件文本设置,代码以下:

?
1
2
3
4
5
6
7
<code><code><code> Runnable runnableTxt = new Runnable() {                       
@Override        
public void run(){                 
// TODO Auto-generated method stub                                
helloBugben.setTxt(bugben_txt);    
  
     };</code></code></code>

3) 在具体测试方法中经过调用runnable对象,实现文本设置,代码以下:

?
1
2
3
4
5
6
7
<code><code><code> new Thread(){   
public void run() {                                 
if (handler != null ) {                                                   
handler.post(runnableTxt);                  
          }                                 
     }           
}.start(); </code></code></code>

咱们运行一下结果,结果截图以下:
这里写图片描述

能够看到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 com.ringo.bugben;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioButton;
public classMainActivityextendsActivity{      
private EditText editText1 = null ;        
private EditText editText2 = null ;      
private RadioButton bold = null ;      
private RadioButton  small = null ;     
private Button button = null ;       
@Override   
protected void onCreate(Bundle savedInstanceState){          
super .onCreate(savedInstanceState);           
setContentView(R.layout.main);           
editText1 = (EditText)findViewById(R.id.editText1);        
editText2 = (EditText)findViewById(R.id.editText2);       
button = (Button)findViewById(R.id.mybutton1);        
bold = (RadioButton)findViewById(R.id.radioButton1);              
small = (RadioButton)findViewById(R.id.radioButton3);                    
button.setOnClickListener( new OnClickListener(){                      
@Override                     
publicvoidonClick(View v){               
Log.v( "Ringo" , "Press Button" );                                   
String isBold = bold.isChecked() ? "bold" : "notbold" ;           
  String wordSize = small.isChecked() ? "small" : "big" ;            
// TODO Auto-generated method stub                                
Intent intent = new Intent(MainActivity. 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 com.ringo.bugben;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextPaint;
import android.widget.TextView;
public classOtherActivityextendsActivity{      
private TextView textView2 = null ;       
private TextView textView3 = null ;        
Boolean bugben_bold = true ;