手把手教你Android标准App的四大自动化测试法宝

作者:Ringoyan,腾讯测试开发工程师。先后为植物大战僵尸Online,糖果传奇等游戏担任测试经理,其负责的“我叫MT2”测试项目曾获腾讯互动娱乐精品文化奖银奖。目前担任腾讯WeTest测试经理。擅长领域:App的自动化测试和Web的安全测试工作。

注:核心内容转自许奔的《深入理解Android自动化测试》,本书将在许奔公众号“巴哥奔”中全文连载。

商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。

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

# Start Script
type = user    #指明脚本类型
count = 10     #脚本执行次数
speed = 1.0    #命令执行速率
start data >>  #用户脚本入口,下面是用户自己编写的脚本```

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

![](http://upload-images.jianshu.io/upload_images/1944350-04000f883b90cd4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](http://upload-images.jianshu.io/upload_images/1944350-258fbd5d09060de5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)




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)```

将上述代码另存为HelloMonkey文件,然后将该脚本推送到手机的sd卡里。

adb push HelloMonkey /mnt/sdcard/

然后运行:

adb shell monkey -v -f /mnt/sdcard/HelloMonkey 1

脚本后面的数字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实现的自动化脚本。

# 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.'

将脚本保存为HelloMonkeyRunner.py,并和Bugben.apk一起拷贝到Android SDK的tools目录下,执行monkeyrunner HelloMonkeyRunner.py


执行完成后,效果如上,并且会在当前目录生成HelloMonkeyRunner.png截图。
MokeyRunner的录制回放
首先是环境配置,在源码“~\sdk\monkeyrunner\scripts”目录下有monkey_recorder.py和monkey_playback.py,将这两个文件(附件中有这两文件)拷贝到SDK的tools目录下,就可以通过如下代码进行启动:

monkeyrunner monkey_recorder.py

运行结果如下图所示:


下面用MonkeyRecorder提供的控件,来进行脚本的录制。


录制完成后,导出脚本保存为HelloMonkeyRunnerRecorder.mr,用文本编辑器打开代码如下:

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',}

脚本录制完毕,接来下看看回放脚本是否正常。回放脚本时执行以下命令:
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,代码如下:
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);     
      }
}

这个程序的功能很简单,就是给2个TextView的内容设置不同的文本格式。

  1. 对于测试工程师而言,HelloBugben是一个已完成的项目。接下来需创建一个测试项目,选择“New->Other->Android Test Project”,命名为HelloBugbenTest,选择要测试的目标项目为HelloBugben项目,然后点击Finish即可完成测试项目的创建。
![](http://upload-images.jianshu.io/upload_images/1944350-a36c366acd98b643.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可以注意到,该项目的包名自带了com.example.hellobugben.test这个test标签,这就说明该测试项目是针对HelloBugben所设置的。
打开AndroidManifest可看到<Instrumentation>标签,该标签元素用来指定要测试的应用程序,自动将com.example.hellobugben设为targetPackage对象,代码清单如下:

<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"    
package="com.example.hellobugben.test"     
android:versionCode="1"      
android:versionName="1.0" >       
<uses-sdkandroid:minSdkVersion="8" />       
<instrumentation            
android:name="android.test.InstrumentationTestRunner"              
android:targetPackage="com.example.hellobugben" />      
<application             
android:icon="@drawable/ic_launcher"          
android:label="@string/app_name" >           
<uses-libraryandroid:name="android.test.runner" />    
</application></manifest>

在<Instrumentation>标签中,android:name声明了测试框架,android:targetPackage指定了待测项目包名。
下面来看一下,如何用Instrumentation框架编写测试程序,代码如下:

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);        
   }    
     };
}

上述代码中,我们首先引入import android.test.ActivityInstrumentationTestCase2。其次让HelloBugbenTestBase继承自ActivityInstrumentationTestCase2<HelloBugbenActivity>这个类。接着在setUp()方法中通过getActivity()方法获取待测项目的实例,并通过textview1和textview2获取两个TextView控件。最后编写3个测试用例:控制文本设置测试testSetText()、字体加粗属性测试testSetBold、字体大小属性测试testSetSize()。这里用到的关键方法是Instrumentation API里面的getActivity()方法,待测的Activity在没有调用此方法的时候是不会启动的。
眼尖的小伙伴可能已经发现控制文本设置测试这里启用了一个新线程,这是因为在Android中相关的view和控件不是线程安全的,必须单独在新的线程中做处理,不然会报

android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views

这个错误。所以需要启动新线程进行处理,具体步骤如下:

  1. 在setUp()方法中创建Handler对象,代码如下:
public void setUp() throws Exception{           
 super.setUp();            
 handler = new Handler(); 
   }
  1. 创建Runnable对象,在Runnable中进行控件文本设置,代码如下:
 Runnable runnableTxt = new Runnable() {                        
@Override         
public void run(){                  
// TODO Auto-generated method stub                                 
helloBugben.setTxt(bugben_txt);     
  }  
    };
  1. 在具体测试方法中通过调用runnable对象,实现文本设置,代码如下:
 new Thread(){    
public void run() {                                  
if (handler != 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<T>

ActivityInstrumentationTestCase2允许InstrumentationTestCase. launchActivity来启动被测试的Activity。而且ActivityInstrumentationTestCase2还支持在新的UI线程中运行测试方法,能注入Intent对象到被测试的Activity中,这样一来,我们就能直接操作被测试的Activity了。正因为ActivityInstrumentationTestCase2有如此出众的有点,它才成功取代了比它早出世的哥哥:ActivityInstrumentationTestCase,成为了Instrumentation测试的基础。

Instrumentation测试框架实战

了解完Instrumentation的基本测试方法后,我们来看一下如何运用Instrumentation框架完成前文Monkey父子完成的自动化测试任务。

  1. 首先建立项目名为Bugben的Project,类名为MainActivity,代码如下:
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);        
     }           
  });   
 }
}
  1. 在建立一个名为OtherActivity的类,点击提交按钮后,跳转到这个界面,代码如下:
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;      
Boolean bugben_notbold = false;         
Float bugben_small_size = (float)20.0;      
Float bugben_big_size = (float)60.0;          
@Override   protectedvoidonCreate(Bundle savedInstanceState){     
super.onCreate(savedInstanceState);        
setContentView(R.layout.other);         
textView2 = (TextView)findViewById(R.id.textView2);    
textView3 = (TextView)findViewById(R.id.textView3);                
Intent data = getIntent();       
textView2.setText(data.getStringExtra("text1"));     
textView3.setText(data.getStringExtra("text2"));      
if (data.getStringExtra("isBold").equalsIgnoreCase("bold")) {                
TextPaint tPaint = textView2.getPaint();                           
tPaint.setFakeBoldText(bugben_bold);            
}else{             
TextPaint tPaint = textView2.getPaint();                           tPaint.setFakeBoldText(bugben_notbold);          
   }           
 if (data.getStringExtra("wordSize").equalsIgnoreCase("small")) {       
TextPaint tPaint = textView3.getPaint();                     
tPaint.setTextSize(bugben_small_size);         
}else{                   
TextPaint tPaint = textView3.getPaint();                          
tPaint.setTextSize(bugben_big_size);        
   }    
 }
}

3.接下来需创建一个测试项目,命名为BugbenTestBase,选择要测试的目标项目为Bugben项目,然后点击Finish即可完成测试项目的创建。

在com.ringo.bugben.test包中添加BugbenTestBase这个类,类的代码如下:

package com.ringo.bugben.test;
import com.ringo.bugben.MainActivity;
import com.ringo.bugben.OtherActivity;
import com.ringo.bugben.R;
import android.app.Instrumentation.ActivityMonitor;
import android.content.Intent;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.text.TextPaint;
import android.util.Log;
import android.widget.Button;import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.TextView;
public class BugbenTestBase extends ActivityInstrumentationTestCase2<MainActivity>{        
publicBugbenTestBase(){            
super(MainActivity.class);     
  }             
MainActivity mainActivity;   
OtherActivity otherActivity;        
private EditText txt1;     
private EditText txt2;      
private RadioButton bold;      
private RadioButton notbold;     
private RadioButton small;      
private RadioButton big;     
private Button subButton;     
private TextView textView1;     
private TextView textView2;      
// 输入值      
String bugben_txt1 = "RingoYan";    
String bugben_txt2 = "自动化测试";    
Boolean bugben_bold = true;       
Boolean bugben_notbold = false;      
Float bugben_small_size = (float)20.0;     
Float bugben_big_size = (float)60.0;        
@Override      
public void setUp() throws Exception{                
super.setUp();                           
// 启动MainActivity              
Intent intent = new Intent();           
intent.setClassName("com.ringo.bugben", MainActivity.class.getName());                 
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);             
mainActivity = (MainActivity)getInstrumentation().startActivitySync(intent);             
// 通过mainActivity的findViewById获取MainActivity界面的控件                      
txt1 = (EditText)mainActivity.findViewById(R.id.editText1);                      
txt2 = (EditText)mainActivity.findViewById(R.id.editText2);                    
bold = (RadioButton)mainActivity.findViewById(R.id.radioButton1);                    
notbold = (RadioButton)mainActivity.findViewById(R.id.radioButton2);               
small = (RadioButton)mainActivity.findViewById(R.id.radioButton3);                     
big = (RadioButton)mainActivity.findViewById(R.id.radioButton4);                     
subButton = (Button)mainActivity.findViewById(R.id.mybutton1);    
   }                      
@Override         
publicvoidtearDown()throws Exception{              
super.tearDown();       
   }                  
// 提交测试     
public void testSubmit()throws Throwable{            
Log.v("Ringo", "test normal submit");              
// 添加一个监听器,监视OtherActivity的启动                    
ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(                    
OtherActivity.class.getName(), null, false);             
// 要操作待测程序的UI必须在runTestOnUiThread中执行             
runTestOnUiThread(new Runnable() {                              
@Override                         
publicvoidrun(){                 
// TODO Auto-generated method stub                              
txt1.setText(bugben_txt1);                                  
txt2.setText(bugben_txt2);                                  
bold.setChecked(true);                                      
big.setChecked(true);                                                                
// 等待500毫秒,避免程序响应慢出错                             
SystemClock.sleep(500);                                                                  
// 点击提交按钮                  
subButton.performClick();          
      }              
  });                           
// 从ActivityMonitor监视器中获取OtherActivity的实例           
otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);                
// 获取的OtherActivity实例应不为空         
assertTrue(otherActivity != null);                          
textView1 = (TextView)otherActivity.findViewById(R.id.textView2);                  
textView2 = (TextView)otherActivity.findViewById(R.id.textView3);         
assertEquals(bugben_txt1, textView1.getText().toString());                 
assertEquals(bugben_txt2, textView2.getText().toString());                            
TextPaint tp = textView1.getPaint();        
Boolean cmpBold = tp.isFakeBoldText();          
assertTrue(cmpBold);            
Float cmpSize = textView2.getTextSize();         
assertTrue(cmpSize.compareTo(bugben_big_size) == 0);             
// 等待500毫秒,避免程序响应慢出错                
SystemClock.sleep(5000);   
      }
}

上述代码中,共包括自动化测试需要进行的5个步骤,具体如下:
(1) 启动应用:通过Intent对象setClassName()方法设置包名和类名,通过setFlags()方法设置标示,然后通过getInstrumentation()的startActivitySync(intent)来启动应用,进入到主界面。
(2) 编辑控件:在Android中相关的view和控件不是线程安全的,所以必须单独在新的线程中做处理。代码中我们在runTestOnUiThread(new Runnable())中的run()方法中执行的。
(3) 提交结果:点击提交按钮进行结果的提交,由于点击按钮也属于界面操作,所以也需要在runTestOnUiThread这个线程中完成。
(4) 界面跳转:这是Instrumentation自动化测试中最需要注意的一个点,特别是如何确认界面已经发生了跳转。在Instrumentation中可以通过设置Monitor监视器来确认。代码如下:

ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(        
OtherActivity.class.getName(), null, false);

然后通过waitForMonitor方法等待界面跳转。

otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);

若返回结果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两个文件。

  1. 设置完成后,可以开始编写项目测试的代码,具体如下:
package com.ringo.bugben.test;
import java.io.File;import android.util.Log;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;;
public class BugBenTest extends UiAutomatorTestCase{   
public BugBenTest (){      super();  }   
String bugben_txt1 = "xiaopangzhu";  
String bugben_txt2 = "bugben";  
String storePath = "/data/local/tmp/displayCheck.png";  
String testCmp = "com.ringo.bugben/.MainActivity";   
@Override  
public void setUp ()throws Exception{      
super.setUp();      
// 启动MainActivity       
startApp(testCmp);  }   
private int startApp(String componentName){      
StringBuffer sBuffer = new StringBuffer();      
sBuffer.append("am start -n ");      
sBuffer.append(componentName);      
int ret = -1;     
try {        
Process process = Runtime.getRuntime().exec(sBuffer.toString());        
ret = process.waitFor();      
} 
catch (Exception e) {        
// TODO: handle exception         
e.printStackTrace();    
  }      
return ret; 
 }   
@Override   
public void tearDown()throws Exception{      
super.tearDown();  
}   
// 提交文字测试   
public void testSubmitTest() throws UiObjectNotFoundException{Log.v  ("Ringo", "test change the textview's txt and size by UIAutomator");   
// 获取文本框1并赋值   
UiObject bugben_et1 = new UiObject(new UiSelector().text("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");  }  
// 获取文本框2并赋值   
UiObject bugben_et2 = new UiObject(new UiSelector().text("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");  
}   
// 获取加粗选项并赋值   UiObject bugben_bold = new UiObject(new UiSelector().text("加粗"));  
if(bugben_bold.exists() && bugben_bold.isEnabled()){     
bugben_bold.click();  
}else{     
Log.e("Ringo", "can not find 加粗");  
}   
// 获取大号字体选项并赋值   
UiObject bugben_big = new UiObject(new UiSelector().text("大号"));  
if(bugben_big.exists() && bugben_big.isEnabled()){     
bugben_big.click();  
}else{     
Log.e("Ringo", "can not find 大号");  }       
// 获取提交按钮并跳转   
UiObject subButton = new UiObject(new UiSelector().text("提交"));  
if(subButton.exists() && subButton.isEnabled()){    
subButton.clickAndWaitForNewWindow();  
}else{    
Log.e("Ringo", "can not find 提交");}     
// 获取文本框1文本   
UiObject bugben_tv1 = new UiObject(new UiSelector()   
.className("android.widget.LinearLayout")   
.index(0)
.childSelector(new UiSelector()   
.className("android.widget.FrameLayout")   
.index(1))   
.childSelector(new UiSelector()  
.className("android.widget.TextView")   
.instance(0)));      
// 获取文本框2文本    
UiObject bugben_tv2 = new UiObject(new UiSelector()   
.className("android.widget.LinearLayout")   
.index(0).childSelector(new UiSelector()   
.className("android.widget.FrameLayout")   
.index(1))   
.childSelector(new UiSelector()   
.className("android.widget.TextView")   
.instance(1)));      
// 验证    
if (bugben_tv1.exists() && bugben_tv1.isEnabled()) {       
assertEquals(bugben_txt1, bugben_tv1.getText().toString());   
}else{       
Log.e("Ringo", "can not find bugben_tv1"); 
  }   
if (bugben_tv2.exists() && bugben_tv2.isEnabled()) {       
assertEquals(bugben_txt2, bugben_tv2.getText().toString());   
}else{       
Log.e("Ringo", "can not find bugben_tv2"); 
  }   
// 截图    
File displayPicFile = new File(storePath);   
Boolean displayCap = UiDevice.getInstance().takeScreenshot(displayPicFile);   
assertTrue(displayCap);  
 }
}

上述代码中,我们首先引入import com.android.uiautomator.testrunner.UiAutomatorTestCase类,并让BugbenTest继承自UiAutomatorTestCase这个类。同样,我们来看下UiAutomator框架下自动化测试进行的5个步骤,具体如下:
(1) 启动应用:于Instrumentation框架不同,UiAutomator是通过命令行进行应用启动的。
am start –n 包名/.应用名

(2) 编辑控件:UiAutomator框架中,控件的编辑相对简单,直接通过UiSelector的text()方法找到对应的控件,然后调用控件的setText()即可对其赋值。

UiObject bugben_et1 = new UiObject(new UiSelector().text("Ringoyan"));           
if(bugben_et1.exists() && bugben_et1.isEnabled()){              
bugben_et1.click();               
bugben_et1.setText(bugben_txt1);    
  }

(3) 提交结果:点击提交按钮进行结果的提交,也是通过UiSelector的text()方法找到对应的控件,然后调用clickAndWaitForNewWindow()方法来等待跳转完成。

UiObject subButton = new UiObject(new UiSelector().text("提交"));                 
if(subButton.exists() && subButton.isEnabled()){                
subButton.clickAndWaitForNewWindow();            
  }

(4) 界面跳转元素获取:用uiautomatorviewer捕捉跳转后的控件,例如捕捉跳转后的文本1:

UiObject bugben_tv1 = new 
UiObject(new UiSelector()  
.className("android.widget.LinearLayout")  
.index(0)  
.childSelector(new UiSelector()  
.className("android.widget.FrameLayout")  
.index(1))  
.childSelector(new UiSelector()  
.className("android.widget.TextView")  
.instance(0)));

(5) 验证显示:跳转后,通过assertEquals()或assertTrue()方法来判断显示的正确性。

if (bugben_tv1.exists() && bugben_tv1.isEnabled())    
{assertEquals(bugben_txt1, bugben_tv1.getText().toString());}

至此核心代码部分已编写完毕。UIAutomator有一个麻烦之处:没法通过Eclipse直接编译。可以借助于一系列命令行进行编译,详细步骤如下:

  1. 通过如下命令创建编译的build.xml文件
    <android-sdk目录>/tools/android create uitest-project –n 工程名 –t 1 –p 项目路径
    针对我们的项目,命令如下:
    android create uitest-project –n BugBenTestUIAuto –t 1 –p "E:\workspace\BugBenTestUIAuto"

创建完成后,刷新BugBenTestUIAuto项目,得到如下图:

打开build.xml会看到,编译项目名为BugBenTestUIAuto。

  1. 设置SDK的路径:
set ANDROID_HOME="E:\sdk\android-sdk-windows"
  1. 进入测试目录,然后进行编译:
cd /d E:\workspace\android\BugBenTestUIAutoant build

编译完成后,再次刷新项目,你将看到BugBenTestUIAuto.jar包生成在bin目录下了,如图:

  1. 将生成的jar包推送到手机端
adb push E:\workspace\android\BugBenTestUIAuto\bin\BugBenTestUIAuto.jar /data/local/tmp/
  1. 在手机端运行自动化脚本,即jar包中的测试用例,命令行如下:
adb shell uiautomator runtest BugBenTestUIAuto.jar -c com.ringo.bugben.test.BugBenTest

运行结果如下,返回OK表示运行成功。

  1. 最后,将运行后的截图从手机端拷贝到PC上
adb pull /data/local/tmp/displayCheck.png E:\workspace\android\BugBenTestUIAuto

至此整个代码就编译和运行完毕,如果觉得调试时反复修改和编译比较麻烦,可以将以上脚本写成一个批处理文件。
UIAutomator工具总结
相比于Instrumentation工具,UIAutomator工具更灵活一些,它不需要项目源码,拥有可视化的界面和可视化的树状层级列表,极大降低了自动化测试脚本开发的门槛。并且UIAutomator支持多应用的交互,弥补了Instrumentation工具的不足。但UIAutomator难以捕捉到控件的颜色、字体粗细、字号等信息,要验证该类信息的话,需要通过截图的方式进行半自动验证。同时,UIAutomator的调试相比Instrumentation要困难。所以在平时的测试过程中,建议将两者结合起来使用,可达到更佳的效果!

注:核心内容转自许奔的《深入理解Android自动化测试》

关于腾讯WeTest (wetest.qq.com)

腾讯WeTest是腾讯游戏官方推出的一站式游戏测试平台,用十年腾讯游戏测试经验帮助广大开发者对游戏开发全生命周期进行质量保障。腾讯WeTest提供:适配兼容测试;云端真机调试;安全测试;耗电量测试;服务器性能测试;舆情监控等服务。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,804评论 25 707
  • 解放程序猿宝贵的右手(或者是左手) ——Android自动化测试技巧 Google大神镇楼 : http://de...
    eclipse_xu阅读 4,934评论 6 40
  • 标签(空格分隔): Android 单元测试的好处:Martin Fowler在《重构》里面还解释了为什么单元测试...
    背影杀手不太冷阅读 5,816评论 3 25
  • Instrumentation介绍 Instrumentation是个什么东西? Instrumentation测...
    打不死的小强qz阅读 7,772评论 2 39
  • 我以前常说我要去漂泊 我喜欢漂泊 而在漂泊过的这两年里我逐渐爱上了乡下 我想有一片地 当然最好面向一面海 或者一汪...
    soumnstan阅读 382评论 0 1