我们很多人应该都知道在Android开发中有两种线程,一种是主线程又称为UI线程,另一个是子线程后称为后台工作线程,Android程序的有部分代码是必须执行在主线程的比如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI绘制以及闹钟事件等等。一旦我们在主线程里面添加了操作复杂的代码,或者线程阻塞的任务,这些代码就很可能阻碍主线程去响应交互事件,有可能倒置在16ms内不能完成单次刷新的操作。导致掉帧的现象,用户就有可能感知到卡顿现象了。
在开发中为了让我们的Apk运行地更加流畅,我们往往需要使用多线程技术让耗时的操作运行在后台。

一旦我们在主线程里面添加了操作复杂的代码,这些代码就很可能阻碍主线程去响应点击/滑动事件,阻碍主线程的UI绘制等等。我们知道,为了让屏幕的刷新帧率达到60fps,我们需要确保16ms内完成单次刷新的操作。一旦我们在主线程里面执行的任务过于繁重就可能导致接收到刷新信号的时候因为资源被占用而无法完成这次刷新操作,这样就会产生掉帧的现象,刷新帧率自然也就跟着下降了(一旦刷新帧率降到20fps左右,用户就可以明显感知到卡顿不流畅了)。

众所周知,Android程序的大多数代码操作都必须执行在主线程,例如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI绘制以及闹钟事件等等。那么我们在上述事件或者方法中插入的代码也将执行在主线程。

ANR

什么是ANR

ANR 是 Application Not Response的简称,当某个应用处于长期假死状态,Android系统会弹出如下窗口,显示XXX 无响应 并给出两个按钮,要求用户选择关闭应用,还是继续等待。

在Android中对未响应定义如下:

Activity对一个输入事件在5秒内没有响应,
Broadcast Receiver在10秒内没有完成它的onReceiver处理程序.

造成ANR的原因及如何避免:

所有的Android应用程序组件(包括Activity,Service,BroadCast Receiver)都在应用程序主线程中运行,因此任何组件中的费时操作都可能阻塞其他组件,为了确保应用程序能够快速响应应用程序交互或者系统事件,必须将所有的费时处理从应用程序主线程移动到子线程中。

Android多线程机制

为了避免产生ANR现象,在应用程序中通过创建工作线程,将耗时操作移动到工作线程中完成,但是在应用程序组件中Notification和Intent总是在GUI线程(主线程)中进行接收和处理,在GUI线程(主线程)中创建的View或者Toast的显示都应该在GUI线程(主线程)中完成。解决这个矛盾。Android采用了Handler机制来实现工作线程与GUI线程同步,从而更新UI。该模型如下图所示:线程通过Looper建立自己的消息循环,消息队列是一个先入先出的队列,工作线程可以往指定的Handler对象投递消息,主线程的Looper负责从消息队列中取出Message并分发到消息指定目标Handler对象进行处理。

Handler 对象


Handler负责往消息队列上添加消息,处理消息,一个线程可以有多个Handler,当消息队列循环到某个Message的时候,便调用对应Handler的handlerMessage()方法对其进行处理。
Handler可投递的对象有“消息对象”和“线程对象”,具体方法如下:

  • 投递消息:
    sendMessage(runable)
    sendMessageAtTime(runable,long)
    sendMessageDelayed(runable,long)

  • 将消息对象从消息队列中移除:
    removeMessage(what)

  • 投递线程对象:
    post(runable)
    postAtTime(runable,long)
    postDelayed(runable,long)

  • 将线程对象从消息队列中移除:
    removeCallbacks(runable)
    主线程从消息队列中取出线程对象后并未开启新的线程来完成线程对象中的任务,而是仍然在当前线程中执行,只是调用了线程对象的run方法。

Looper 对象


Looper是用来封装消息循环和消息队列的一个类,一个Looper对应一个消息队列,一个线程最多只能有一个Looper,默认情况下Android会为主线程创建Looper并开启消息循环,但是使用Thread创建的工作线程没有开启消息循环,所以,在主线程中,应用下面代码创建Handler对象时,就不会出错;而如果在新创建的非线程中,应用下面的代码就会产生异常。

Handler  handler = new Handler();

如果想要在非主线程中创建Handler对象,首先需要使用Looper类的prepare()方法来初始化一个Looper对象,然后创建一个Handler对象,再使用Looper类的looper()方法启动Looper,从消息队列里获取和处理消息。写在Looper.loop()之后的代码不会被执行,这个函数的内部是一个循环,当调用Handler.getLooper().quit()方法之后,loop()方法才会终止,后面的代码才能运行。

public class LooperThread extends Thread {
@Override
public void run() {
// 将当前线程初始化为Looper线程
Looper.prepare();

// ...其他处理,如实例化handler

// 开始循环处理消息队列
Looper.loop();
}
}

Looper 有如下方法可以使用:

Message 对象


在创建Looper对象的时候会创建一个消息队列来容纳消息,每个线程只可以拥有一个消息队列,其中的Message是由Looper来分发的,同时Message不能直接添加到消息队列中,只能通过与Looper关联的Handler来添加。

Messsage对象可以通过如下两类方法获取:

  1. 通过Message构造方法新建一个Message对象
  2. 通过Message.obtain方法或者Handle.obtainMessage方法来获取

后者并不一定直接创建新的实例,而是查看消息池中是否有满足要求的Message实例,存在则直接取出并返回这个实例,如果没有,则使用给定的参数创建一个Message对象。

Message msg = handler.obtainMessage(...........);
msg.sendToTarget();
Message msg = Message.obtain(........);
handler.sendMessage(msg)
Message msg = new Message()
handler.sendMessage(msg)

除了使用arg1,arg2,obj作为数据载体外还可以使用bundle来携带数据

Bundle bundle = new Bundle();
bundle.putString("title",title[index]);
msg.setData(bundle);

使用Thread创建线程

  1. 通过Thread类的构造方法创建线程
Thread  thread = new Thread(new Runnable(){
public void run(){

}
}).start();

2.通过实现Runnable接口创建线程

public class MainActivity extends Activity implements Runnable{
public void run(){
}
}

Thread 常用方法

一个例子
public class MainActivity extends Activity implements OnClickListener {

private static final int MAIN2WORK = 1;
private static final int WORK2MAIN = 2;
private Button mMain2workBtn = null;
private Button mWork2mainBtn = null;
private Handler mMainHandler = null;
private WorkThread thread = null;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMain2workBtn = (Button) findViewById(R.id.main2workBtn);
mWork2mainBtn = (Button) findViewById(R.id.work2mainBtn);
mMain2workBtn.setOnClickListener(this);
mWork2mainBtn.setOnClickListener(this);
thread = new WorkThread();
thread.start();
mMainHandler = new Handler(){
public void handleMessage(Message msg) {
switch (msg.what) {
case WORK2MAIN:
Log.i("MainActivity",(String) msg.obj);
break;
default:
break;
}
super.handleMessage(msg);
}
};
}

public void onClick(View v) {
switch (v.getId()) {
case R.id.main2workBtn:
Message msgs = Message.obtain();
msgs.what = MAIN2WORK;
Bundle b = new Bundle();
b.putString("Message","Hello");
b.putString("From","MainThread");
msgs.setData(b);
thread.getmWorkThreadHandler().sendMessage(msgs);
break;
case R.id.work2mainBtn:
Message msg = mMainHandler.obtainMessage();
msg.what= WORK2MAIN;
String sayHi = "Hello This message is From WorkThread!";
msg.obj = sayHi;
msg.sendToTarget();//因为是从mMainHandler中获得的所以这里可以直接调用sendToTarget()
break;
default:
break;
}
}
}

private static class WorkThread extends Thread{
private static final int MAIN2WORK = 1;
private Handler mWorkThreadHandler = null;
public void run() {
super.run();
Looper.prepare();
mWorkThreadHandler = new Handler(){
public void handleMessage(Message msg) {
switch (msg.what) {
case MAIN2WORK:
Bundle b = msg.getData();
Log.i("MainActivity",b.getString("Message")+":"+ b.getString("From"));
break;
default:
break;
}
};
};
Looper.loop();
Log.i("MainActivity","这个字符串不会显示");
}
public Handler getmWorkThreadHandler() {
return mWorkThreadHandler;
}
}

使用HandlerThread创建线程


HandlerThread用于创建线程,与Thread不同的是HandlerThread类能够创建一个含有Looper的线程
生成一个HandlerThread对象

HandlerThread task = new HandlerThread(“thread”);

在调用HandlerThread.getLooper方法前,必须调用该类的start方法,不然会报空指针

task.start();
looper = mHandlerThread.getLooper();
mHandler = new Handler(looper){
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
Message msg = myHandler.obtainMessage();
………………
msg.sendtoTaget();
一个例子

使用post提交Runable线程对象,在Runable线程对象的run方法中输出当前线程的ID。代码如下,通过该实验可以发现Handler只是调用Runable线程对象的run方法,所有的操作都在主线程中执行,并没有创建一个新的进程。要想让投递的方法在子线程中运行就要用到HandlerThread类。下一个实验所示。

public class MainActivity extends Activity implements OnClickListener {

private Button mPostRunableBtn = null;
private Handler mHandler = null;
private TextView mShowMainTV = null;
private Runnable mRunableThread = new Runnable() {
public void run() {
Log.i("MainActivity", "当前线程名 :"+Thread.currentThread().getName()+" 当前线程ID:"
+ Thread.currentThread().getId());
}
};

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPostRunableBtn = (Button) findViewById(R.id.postRunableBtn);
mShowMainTV = (TextView) findViewById(R.id.showMainIDTV);
mPostRunableBtn.setOnClickListener(this);
mShowMainTV.setText("主线程ID:" + Thread.currentThread().getId());
mHandler = new Handler() ;
}

public void onClick(View v) {
switch (v.getId()) {
case R.id.postRunableBtn:
mHandler.post(mRunableThread);
break;
default:
break;
}
}
}

另一个例子

使用post提交Runable线程对象,与上面的例子比较,不同的是该实验往HandlerThread投递。这时候Runable线程对象运行在新的线程上。代码如下,结果如下

public class MainActivity extends Activity implements OnClickListener {

private Button mHandlerThreadBtn = null;
private TextView mShowMainIDTV = null;
private HandlerThread mHandlerThread = null;
private Handler mHandler = null;
private Looper loopers = null;

private Runnable mRunableThread = new Runnable() {
public void run() {
Log.i("MainActivity","当前线程名 :" + Thread.currentThread().getName()
+ " 当前线程ID:" + Thread.currentThread().getId());
}
};

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandlerThreadBtn = (Button) findViewById(R.id.handlerThreadBtn);
mShowMainIDTV = (TextView) findViewById(R.id.showMainIDTV);
mHandlerThreadBtn.setOnClickListener(this);
mShowMainIDTV.setText("主线程ID: " + Thread.currentThread().getId());
mHandlerThread = new HandlerThread("thread");
mHandlerThread.start();
loopers = mHandlerThread.getLooper();
mHandler = new Handler(loopers);
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.handlerThreadBtn:
mHandler.post(mRunableThread);
break;
default:
break;
}
}
}

AsyncTask


AsyncTask允许定义将在后台执行的操作,并提供了可以用来监控进度以及在GUI线程上发布结果的方法。
AsyncTask处理线程的创建,管理和同步等全部工作,它可以创建一个异步任务,该任务由两个部分组成,将在后台执行到处理以及在处理完成后执行到UI更新。对于生命周期较短并且需要在UI上显示进度和结果到后台操作是很好的解决方案。但是AsyncTask在设备的方向发生变化而导致Activity被摧毁和重新创建时候会被取消,因此对于生命周期较长的后台操作,使用Service比较合适.

使用AsyncTask进行异步处理
  1. 继承AsyncTask:
public class Mytask extends AsyncTask<输入变量类型,更新变量类型,结果值类型>
  1. 重写相关方法:
  • doInBackground:这个方法将会在后台线程执行,可以把运行时间较长的代码放置到这里,而且不能试图在此处理程序中与UI对象交互,可以调用publishProcess方法以传递参数值给onProgressUpdate,当后台任务完成后,可以返回最终结果并作为参数传递给onPostExecute处理程序,在该处理程序中更新UI
  • onProgressUpdate 当中间进度更新变化的时候更新UI,
  • onPostExecute当doInBackground完成后该方法的放回值就会传入到这个事件处理程序中,在这个处理程序中可以更新UI
  1. 运行异步任务:
new Mytask().execute(Inputparam);

  1. 判断AsyncTask是否正在运行
    if(mTask!=null&&mTask.getStatus() == AsyncTask.Status.RUNNING){
    }
  2. 取消AsyncTask:
mTask.cancel(true);
  1. 一个AsyncTask只能使用一次,当你想再次使用的话,只能再new一个,否则会出现如下错误:
java.lang.IllegalStateException: Cannot execute task: the task has already been executed (a task can be executed only once)
AsyncTask使用时需要注意的问题:
  • 默认情况下,AsyncTask任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。一旦其中的某个AsyncTask执行时间过长,队列中的其他剩余AsyncTask都处于阻塞状态,必须等到该任务执行完毕之后才能够有机会执行下一个任务。情况如下图所示:

为了解决上面提到的线性队列等待的问题,我们可以使用AsyncTask.executeOnExecutor()强制指定AsyncTask使用线程池并发调度任务。

  • 在把AsyncTask写成Activity的内部类的时候就很容易因为AsyncTask生命周期的不确定而导致Activity发生泄漏。

线程池

我们在设计模式中在介绍享元模式的时候介绍了用池技术,在Android中也可以通过线程池将任务分解,并发执行,

下面是来自胡凯博客上的一个例子,在这个例子宏我们需要一次性decode 40张图片,每个线程需要执行4ms的时间,如果我们使用专属单线程的方案,所有图片执行完毕会需要花费160ms(40*4),但是如果我们创建10个线程,每个线程执行4个任务,那么我们就只需要16ms就能够把所有的图片处理完毕。


为了降低开发难度,系统为我们提供了ThreadPoolExecutor帮助类来简化实现。

线程优先级

默认情况下,新创建的线程的优先级和创建它的父线程保持一致。也就是说如果主UI线程创建出了十几个子线程,那么这些子线程的优先级也和默认和主线程保持一致,为了不让新创建的工作线程和主线程抢占CPU资源,需要把这些线程的优先级适当降低,从而提高主线程所能得到的系统资源。
在Android系统里面,我们可以通过android.os.Process.setThreadPriority(int)设置线程的优先级,参数范围从-20到24,数值越小优先级越高。同时也定义了一些特定的优先级供日常使用。

在Release 正式版的Apk之前都需要我们对Apk进行签名,APK签名中包含了我们开发者的有效信息。一些应用市场就可以根据这些信息来判断当前的Apkd是否是个安全的Apk。在我们开发中如果没有做任何设置默认是签Debug名字,签名文件位于:$HOME/.android/debug.keystore
下面将对关于签名的一些总结与大家进行分享:

产生密钥:

keytool -genkey -alias mykey -keyalg RSA -validity 40000 -keystore demo.keystore

#说明:
# -genkey 产生密钥
# -alias mykey 别名 mykey
# -keyalg RSA 使用RSA算法对签名加密
# -validity 40000 有效期限4000天
# -keystore demo.keystore

查看keystore的信息

keytool -list -keystore demo.keystore -alias mykey -v

查看keystore的公钥证书信息

keytool -list -keystore demo.keystore -alias mykey -rfc

查看apk的签名信息

jarsigner -verify -verbose -certs <your_apk_path.apk>

较为安全的签名方式:

下面是我自己目前使用的签名方式:
keystore文件和key密码等配置文件都存在于电脑中的非项目文件夹中,提交到github上面的只有keystore配置文件的路径。

def keystorePropertiesFile = file(keyStorePath);
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']

}
}

在上面keyStorePath所指定的配置文件中存储的如下的变量:

storePassword=xxxx   
keyPassword=xxxx
keyAlias=xxxx
storeFile=xxxxxxxxxx

验证两个Apk是否签名相同

jar tf app-release-unaligned.apk|grep RSA
jar xf app-release-unaligned.apk META-INF/CERT.RSA
keytool -printcert -file META-INF/CERT.RSA
两个apk是否同签名,比较签名的MD5码或SHA1码 ,一样就是相同的,反之,不是

发布版本前的优化配置

buildTypes {
release {
//不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
//开启混淆
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//Zipalign优化
zipAlignEnabled true
//移除无用的resource文件
shrinkResources true
//签名
signingConfig signingConfigs.release
}
}

例子

android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "com.idealist.testleakcandy"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
abortOnError false
}
def keystorePropertiesFile = file('/home/jimmy/Dev/keystore.properties');
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
buildTypes {
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
//混淆
minifyEnabled true
//Zipalign优化
zipAlignEnabled true
// 移除无用的resource文件
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}

  1. 清理代码中的调试信息,如一些非关键的Log(其实这个一般在开发中我们会将其做成可设置的用一个标志位对其进行控制,在释放版本之前需要将其配置为关闭Log)以及StrictMode。
  2. 运行Lint,CheckStyle,findBug等静态代码检查工具,查看代码中可能潜在的Bug并修复。
  3. 在build.gradle文件中修改VersionCode和VersionName,对于数据库,如果需要更新数据库,则需要程序中的数据库版本。
  4. 在清单文件中将项目的debugable设置为false
  5. build.gradle中指定代码混淆文件地址,配置proguard-project.txt文件
  6. 运行自动化测试用例
  7. 将Build Variants 改为release
  8. 在Android Studio中运行assembleRelease 任务
  9. 通过Android studio中的Build -> Create signed Apk 使用已有的签名证书keystore文件对Apk进行签名。
  10. 使用zipalign对Apk进行资源对齐优化
    使用zipalign进行资源对齐优化的必要性
    在Android中,当资源文件通过内存映射对齐到4字节边界时,访问资源文件的代码才是有效率的。但是,如果资源本身未使用zipalign工具对资源进行对齐处理就必须显式地读取,这个过程将会比较缓慢且会花费额外的内存。

对于应用程序开发者来说,这种显式读取方式是相当便利的。它允许使用一些不同的开发方法,包括正常流程中不包含对齐的资源,因此,这种读取方式具有很大的便利性。但是对于用户来说,从未对齐的apk中读取资源比较慢且花费较多内存。导致未对齐的程序启动得比对齐后的慢,甚至由于增加内存压力,从而造成系统反复地启动和杀死进程。最终,应用被用户卸载。
具体操作:

../zipalign -f -v 4 xxx_release_signed_platform.apk xxx_release_signed_platform_zipalign.apk

同时可以利用zipalign工具检查当前APK是否已经执行过Align优化:

zipalign -c -v 4 xxx_release_signed_platform_zipalign.apk
  1. 项目打包,安装测试

说起自动化测试只能用“又恨又爱”,起初接触自动化测试的时候是感入职没多久,公司正在弄自动化测试平台,那时候正需要一批小白鼠,而我们就成为了这些小白鼠,当时面对着覆盖率的压力,对之“恨之入骨”,但是还是度过了那段难熬的时间,而后续进入项目组中由于Get这项技能,在每次遇到不稳定复现的问题都会很容易想起编写自动化测试用例来
复现问题,这些自动化测试用例的好处是在解决完Bug的时候还可以使用这些自动化测试用例来验证时候完全解决了这个问题。但是对测试用例的看法还是:自动化是为开发维护服务的,而不能作为一个KPI强制来完成,因为这些自动化测试用例一般也是需要维护时间成本的,不要为了覆盖率强制写那些充数的自动化测试,好了开始今天的话题吧,
今天将会学习使用Robolectric进行单元测试。为了与时俱进这篇博客也升级到了Robolectric 3.0.Android Studio Preiview 2.2.

单元测试

单元测试中的单元指的是测试的最小模块,通常指的是某些类中的函数。一般有如下三类:
1.有明确的返回值,这类单元测试的时候只需要构造合适的参数,调用对应的方法,获取返回值,比对返回值是否和期待的返回值一致。
2.没有对应的返回值。只改变某些对象的一些属性或者状态,这时候就只能验证这些改变的属性和返回值就可以了。如果没有对象的属性被改变那么只能间接通过验证行为来验证了。
3.没有返回值,没有属性变化,没有外在行为改变的三无方法是无法测试的。
当前的主流单元测试框架有AndroidTest和Robolectric,前者需要运行在Android设备上而后者的最大特点是它:能让你的测试代码运行在JVM上,这就意味着在做单元测试的时候,不必将你的apk安装到机器上运行从而极大得减少了我们通过测试来验证问题的时间。

在一般Android项目开发中我们会使用单元测试来测试函数行为,四大组件状态,控件状态行为,在一些项目中往往会认为单元测试覆盖率越高越好,甚至拿它来作为一个硬性的指标,但是这是不大现实的,首先未必所有的项目周期都允许花那么长的时间来单独做自动化测试。有些函数逻辑并不复杂,并不需要作为测试对象进行测试。

一般我们在写测试用例之前需要确定要测试的测试点,可以用一个列表的形式罗列出测试点,针对每个测试点罗列出所属的测试页面,涉及的界面元素,较为详细的描述,输入数据和输出数据。
在确定测试项列表后就可以进行针对某个页面进行测试了,在进行测试之前需要找到当前页面的入口,并确定跳转到该页面的条件,这些条件一般会写到@Before中。然后分析当前页面的界面元素,业务逻辑,控件行为。在熟悉了上述的逻辑后,就可以建立测试用例对其进行测试了。测试结束后还需要进行覆盖率的统计,看下是否还有未考虑的因素在内。直到上述流程全部完成。

搭建环境

在这种情况下如果要使用robolectric需要在模块的build.gradle中添加如下依赖:

testCompile "org.robolectric:robolectric:3.0"

在test/src目录下创建Robolectric 测试类

@RunWith(RobolectricTestRunner.class)
public class WelcomeActivityTest {
@Test
public void clickingLogin_shouldStartLoginActivity() {
WelcomeActivity activity =
Robolectric.setupActivity(WelcomeActivity.class);
activity.findViewById(R.id.login).performClick();
Intent expectedIntent = new Intent(activity, WelcomeActivity.class);
assertThat(shadowOf(activity).getNextStartedActivity())
.isEqualTo(expectedIntent);
}
}

这里需要用注释注明使用RobolectricGradleTestRunner,注意我们必须指定指向Gradle编译系统产生的BuildConfig.class.Roblectric使用这些常量来获取Gradle编译应用所使用的输出路径。如果没有这些常量,Robolectric将不能找到项目中的manifest, resources,和assets这些资源。

配置Robolectric

最主要的配置Robolectric的方式是通过 @Config注释,这些注释可以用在类级别和方法级别,并且方法级别的注释将会覆盖掉类级别的注释.

  • 配置 SDK Level
    默认情况下Robolectric将会在targetSdkVersion上运行你的测试代码,但是如果你想在不同的sdk等级上运行你的测试代码可以通过如下方式修改:
    @Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
    public class SandwichTest {
    @Config(sdk = Build.VERSION_CODES.KITKAT)
    public void getSandwich_shouldReturnHamSandwich() {
    }
    }
  • 配置Application Class
    Robolectric将会尝试创建在AndroidManifest中指定的Application类的实例,如果你想提供一个自定义的实现我们可以通过如下方式来指定。
    @Config(application = CustomApplication.class)
    public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
    }
    我们通常不需要直接产生ActivityController只需要使用Robolectric.buildActivity()来生成ActivityController,对于那些只需要一个实例化后的Actiivity的情况只需要如下的代码来获取:
    Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();

    Activity实例创建

    这个一般用于获取Activity实例
    MainActivity mainActivity = Robolectric.setupActivity(MainActivty.class);
    assertNotNull(mainActivity);
    assertEquals(mainActivity.getTitle(),"MyApplication");

    Activity跳转测试

    btn.performClick();
    Intent expectedIntent = new Intent(mainActitity,LoginActivity);
    intent actualIntent = ShadowAppication.getInstance().getNextStartedActivity();
    assetEquals(expectedIntent,actualIntent);

    测试Activity生命周期

    要测试Activity的生命周期可以通过如下的方式:
    ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
    Activity activity = controller.get();
    // assert that something hasn't happened
    activityController.resume();
    // assert it happened!
    通过相同的方式还可以测试start(), pause(), stop(), 和destroy()等生命周期。

如果需要测试使用Intent启动Activity可以借助如下方式进行:

Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
Bundle savedInstanceState = new Bundle();
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
.create()
.restoreInstanceState(savedInstanceState)
.get();

UI控件状态测试

CheckBox checkbox = (CheckBox)mainActivity.findViewById(R.id.checkbox);
Button btn = (Button)mainActivity.findViewById(R.id.btn)

checkbox.setChecked(true);
btn.performClick();
assertTrue(!checkbox.isChecked());

Dialog:

AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
assertNotNull(dialog);

Toast:

ShadowToast.getTextOfLatestToast();

BroadcastReceiver的测试:

ShadowApplication shadowApplication = ShadowApplication.getInstance;
Intent intent = new Intent("xxxxxxxx");
assertTrue(shadowApplication.hasReceiverForInstance(intent));

MyReceiver myreceiver = new MyReceiver();
myreceiver.onReceiver(RuntimeEnvironment.application.intent);
测试调用接收器的结果

服务的测试:
MyService service = new MyService();
registrationService.onHandleIntent(new Intent());

资源的使用:

通过一个例子来介绍字符串的使用,下面包含三个目录下的strings.xml,

values/strings.xml
<string name="not_overridden">Not Overridden</string>
<string name="overridden">Unqualified value</string>
<string name="overridden_twice">Unqualified value</string>
values-en/strings.xml
<string name="overridden">English qualified value</string>
<string name="overridden_twice">English qualified value</string>
values-en-port/strings.xml
<string name="overridden_twice">English portrait qualified value</string>

下面使用了@Config(qualifiers=”en-port”)这样就会现在values-en-port中查找,如果没有的话就会在其他的string中找。

@Test
@Config(qualifiers="en-port")
public void shouldUseEnglishAndPortraitResources() {
final Context context = RuntimeEnvironment.application;
assertThat(context.getString(R.id.not_overridden)).isEqualTo("Not Overridden");
assertThat(context.getString(R.id.overridden)).isEqualTo("English qualified value");
assertThat(context.getString(R.id.overridden_twice)).isEqualTo("English portrait qualified value");
}
Shadow Classes

Robolectric 针对Android SDK中的对象定义了很多的shadow类,这些shadow类对Android原先的类进行了修改和扩展。当一个Android对象被实例化的时候Roblectric会寻找相应的shadow类。如果能够找到一个类,它将会产生一个shadow对象与之关联。每次在一个Android类中调用一个方法,Robolectric会确保shadow类的对应的方法会最先被调用。所以这些shadow类中的成员方法将都有一次运行机会。这个适用于所有包括静态和final的方法。

添加行为

如果shadow类提供的行为不是你需要的,但是也许只是在单个测试中,或者一组测试中,或者全部的测试集合这种提供的行为不适合,这时候我们可以简单地重新地声明一个类(比如ShadowFoo),然后在它上面添加@Implements(xxxx.class)这种注释。然后可以在测试方法或者类中添加
@Config(shadows=ShadowFoo.class)。

Shadow Classes

为了Robolectric框架能够实例化Shadow 类,Shadow类通常需要一个公共的非参数的构造函数,
Shadow classes 应该和对应类有对应的继承关系,比如我们需要实现一个ViewGroup的Shadow类,那么我们的shadow类需要继承ViewGroup的父类的Shadow,ShadowView。

@Implements(ViewGroup.class)
public class ShadowViewGroup extends ShadowView {

Shadow Methods

Shadow 对象中实现和Android类中有相同signature的方法,当Android对象的方法被调用的时候这个具有相同signature的方法将会同样被调用。
比如如下代码被调用的时候

this.imageView.setImageResource(R.drawable.pivotallabs_logo);

在对应的测试中ShadowImageView#setImageResource(int resId) 方法将会被调用
Shadow方法必须使用@Implementation进行注释

@Implements(ImageView.class)
public class ShadowImageView extends ShadowView {
...
@Implementation
public void setImageResource(int resId) {
// implementation here.
}
}

我们需要注意Shadow方法需要实现在对应的Shadow类中。否则Robolectric的查询机制将不会找到他们。比如setEnabled()方法是在View中定义.如果setEnabled()在ShadowViewGroup中定义而不是在ShadowView中定义,它将不会被调用。

Shadowing Constructors

一旦Shadow对象被实例化,Robolectric将会寻找和实际类中具有相同参数的的构造函数的__constructor__方法。比如在应用代码中触发了一个接收一个Context的TextView构造方法那么对应的Robolectric将会触发接收一个Context的__constructor__ 方法。

@Implements(TextView.class)
public class ShadowTextView {
...
public void __constructor__(Context context) {
this.context = context;
}
...

Getting access to the real instance

有时Shadow类可能会需要访问他们正在shadowing的实例对象,比如我们需要操纵对应的成员变量。 Shadow 类可以通过声明一个成员变量并使用@RealObject对其进行注释:

@Implements(Point.class)
public class ShadowPoint {
@RealObject private Point realPoint;
...
public void __constructor__(int x, int y) {
realPoint.x = x;
realPoint.y = y;
}
}

在上面的例子中Robolectric 将会在触发任何方法之前将realPoint赋给Point的实例对象。需要注意的是在实际对象中调用的方法依旧会被拦截和重定向。这个特性在测试代码中并没什么影响,但是它在Shadow类的实现中将会有很大影响。

创建自定义Shadows

自定义shadows是Robolectric一个重要的特性,它允许我们修改目标测试Android功能的行为。自定义shadows允许我们在不添加或者修改Robolectric提供的Shadow代码的情况下修改某个方法的行为,它也允许我们的shadow指定的context。

编写自定义 Shadow

自定义shadows在结构上和正常的shadow类有很大的相似性。它必须在类定义中包含@Implements(AndroidClassName.class) 注释,我们可以使用正常的implementation 选项,比如使用@Implementation 实例化方法,或者使用

public void __constructor__(...).

实例化一个构造方法。

@Implements(Bitmap.class)
public class MyShadowBitmap {
@RealObject private Bitmap realBitmap;
private int bitmapQuality = -1;

@Implementation
public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
bitmapQuality = quality;
return realBitmap.compress(format, quality, stream);
}

public int getQuality() {
return bitmapQuality;
}
}
}

自定义TestRunner

public class CustomShadowTestRunner extends RobolectricGradleTestRunner {
public CustomShadowTestRunner(Clas<?> cls) throws initializationError {
super(cls);
}

public InstrumentationConfiguration creatClassLoaderConfig(){
InstrumentationConfiguration.Builder builder = Instrumentation.newBuilder();
builder.addInstrumentatedClass(OriginClass.class.getName());
return builder.build();
}

}

使用自定义Shadow

自定义 Shadows 使用@Config注释和对应的测试类或者测试方法绑定。如果需要使用在上面介绍的MyShadowBitmap类我们需要使用@Config(shadows={MyShadowBitmap.class})这种方式来包含或者使用
@Config(shadows={MyShadowBitmap.class, MyOtherCustomShadow.class})这种方式来包含多个Shadow类。这将会让Robolectric识别并在测试你对应的方法的时候使用这些自定义的Shadow。

@RunWith(CustomShadowTestRunner.class)
@Config(constants = BuildConfig.class,shadows = {ShadowPerson.class})
public class ShadowTest {
@Test
public void testCustomShadowdemo(){
Person person = new Person("CustomShadowTestRunner");
person.xxxxxx //由于绑定了Shadow所以实际上调用的是ShadowPerson对应的方法

//获取对应的Shadow对象
ShadowPerson shadowPerson = (ShadowPerson) ShadowExtractor.extract(person);
shadowPerson.xxxxxxx //调用ShadowPerson对应的方法

}
}

使用Log来调试我们的测试用例

要使用Log功能只需要在每个TestCase的setUp()里执行ShadowLog.stream = System.out即可,

@Before
public void setUp() throws URISyntaxException {
ShadowLog.stream = System.out;
}

关于网络与数据库的测试

如果要学习网络与数据库的测试大家可以移步如下链接:
并且作者有提供了一份很好的学习代码可供学习,个人觉得结合googeSample的android-testging和作者的测试例子就可以较好得掌握测试用例的编写了。

关于测试用例学习的一些较好的源码

这些文章只能帮助我们对一些概念的理解,但是对于程序员来说有一份实际可用的源码进行学习是最好不过了,下面是一些我在学习自动化测试部分时候收集的一些源码。分享给大家,大家可以让这些代码跑起来,在他们的基础上做一些实验就可以更好得掌握自动化测试用例的编写了。

https://github.com/googlesamples/android-testing
https://github.com/geniusmart/LoveUT

较好的文章

[http://www.jianshu.com/p/9d988a2f8ff7]http://www.jianshu.com/p/9d988a2f8ff7
[http://www.jianshu.com/p/3aa0e4efcfd3]http://www.jianshu.com/p/3aa0e4efcfd3
[http://tech.meituan.com/Android_unit_test.html]http://tech.meituan.com/Android_unit_test.html

UiAutomator是一个非常好的黑盒主动化测试框架,可以在不知道目标应用内部具体实现的情况下进行测试,并且它的最重要优点是能够跨进程。而Espresso属于白盒测试框架,需要在有代码的情况下进行测试,并且它不支持跨进程,现在由于使用AndroidJUnitRuner替代了Instrumentaion,将UiAutomator并入AndroidJUnitRuner
从而将二者的优点合并在一起显得更加强大。

我们之前以及介绍了Espresso自动化测试框架,它是一个白盒测试框架,需要在有代码的情况下进行测试,接下来要介绍的UiAutomator2也是一个用于测试功能的框架,和Espresso不同的是UiAutomator是一个黑盒自动化测试框架,可以在不知道代码的情况下进行测试,
它可以跨进程进行测试,但是UiAutomator2 有一些不足比如在无法测试Toast弹出。对Intent测试也不如Espresso强大。Espresso的强大的地方是语法简介,测试运行快,能够测试Toast,以及Intent,使用IdingSource还可以测试异步操作。能不能将二者结合起来进行测试呢?答案是肯定的。
在Android Studio 环境下需要添加UiAutomator只需要添加如下依赖。或者下载离线jar包。

androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.0.0'

UiAutomator2与Espresso的测试思路是一样的,都是查找对象,执行操作,检查操作结果。UiAutomator2主要由UiDevice,UiObject,UiSelector,UiScrollable,UiCollection,UiWatcher这些元素组成。UiAutomator2更是在原先UiAutomation基础上添加了更多的类,我们在稍后的部分将会对其进行展开介绍。

Uidevice

提供关于设备的状态信息。也可以使用这个类来模拟装置上的用户的行为。

  • 初始化UiDevice

    private UiDevice(Instrumentation instrumentation)
    public static UiDevice getInstance(Instrumentation instrumentation)
  • 获取某个对象

    public UiObject findObject(UiSelector selector)
    public boolean hasObject(BySelector selector)
    public UiObject2 findObject(BySelector selector)
    public List<UiObject2> findObjects(BySelector selector)
  • 与屏幕相关

    public void wakeUp() throws RemoteException 唤起界面
    public void sleep() throws RemoteException 休眠设备
    public boolean isScreenOn() throws RemoteException 判断界面是否处于亮屏状态
    public Point getDisplaySizeDp()
    public int getDisplayWidth()获取显示器的宽度,以像素为单位。
    public int getDisplayHeight()获取显示器的高度,以像素为单位。
    public boolean isNaturalOrientation() 判断是否处于原始状态
    public int getDisplayRotation()获取当前界面的旋转状态
    public void freezeRotation() throws RemoteException 禁用传感器和设备的旋转且在当前的旋转屏幕状态冻结
    public void unfreezeRotation() throws RemoteException重新启用传感器和允许物理旋
    public void setOrientationLeft() throws RemoteException 左转屏幕
    public void setOrientationRight() throws RemoteException 右转屏幕
    public void setOrientationNatural() throws RemoteException 将屏幕状态设置为原始位置
    public String getProductName()获取当前产品名
  • 与设备操作相关

    public boolean pressMenu()短按MENU键
    public boolean pressBack()短按返回键.
    public boolean pressHome()短按HOME键.
    public boolean pressSearch()短按搜索键
    public boolean pressDPadCenter()轨迹球
    public boolean pressDPadDown()轨迹球
    public boolean pressDPadUp()轨迹球
    public boolean pressDPadLeft()轨迹球
    public boolean pressDPadRight()轨迹球
    public boolean pressDelete()短按删除键
    public boolean pressEnter()短按回车键.
    public boolean pressKeyCode(int keyCode) 短按键盘代码
    public boolean pressRecentApps() throws RemoteException 短按弹出RecentApp按钮
    public boolean openNotification() 弹出通知栏
    public boolean openQuickSettings() 弹出快速设置
    public boolean click(int x, int y) 在任意坐标指定的坐标执行一个点击
    public boolean swipe(int startX, int startY, int endX, int endY, int steps)
    public boolean drag(int startX, int startY, int endX, int endY, int steps)
    public boolean takeScreenshot(File storePath)
  • 其他

    public boolean waitForWindowUpdate(final String packageName, long timeout) 等待窗口内容更新事件的发生
    public void waitForIdle() 等待当前的应用程序处于空闲状态
    public void waitForIdle(long timeout) 等待当前的应用程序处于空闲状态
    public String getCurrentPackageName() 返回当前界面的包名的字符串
    public String getLauncherPackageName()
  • 执行命令

    public String executeShellCommand(String cmd) throws IOException
  • UiWatcher相关

监听器用于在测试过程中处理可能打断测试的突发事件,当脚本其他未知情况打断执行的时候,如果有监听器则会跳转到监听器中执行,我们可以使用监听器做一些特殊的处理如,来电,闹钟日常等测试。比如我们在测试某个用例这时候突然来了个电话突然进入通话界面,这时候就会导致测试失败,但是如果使用了UIWatcher,就可以跳到UiWatcher进行处理,
。这部分将会在UiWatcher中展开介绍。

public void registerWatcher(String name, UiWatcher watcher) 注册一个监听器,用户处理某个步骤被打断的情况的异常,
public void removeWatcher(String name) 移除之前注册的指定监听器
public void runWatchers() 强制运行所有的监听器
public void resetWatcherTriggers() 重置一个监听器
public boolean hasWatcherTriggered(String watcherName) 检查某个特定的监听器是否触发过
public boolean hasAnyWatcherTriggered()检查是否有监听器触发过
private void setWatcherTriggered(String watcherName)
public <R> R performActionAndWait(Runnable action, EventCondition<R> condition, long timeout)
UiSelector && BySelector && By
UiSelector

UiSelector用于获取操作对象,这个和Espresso withid,withText一样用于匹配某个对象,UiSelector 获取对象一般依具备两类,一类是控件属性,一类是控件层级关系。

  • 在UiAutoMator中用于获取对象的属性有如下几种:
    index
    instance
    class
    package
    Content-desc
    checkable
    checked
    clickable
    enabled
    focusable
    focused
    Scrollable
    Long-clickable
    Password
    Selected
    Bounds
    上述这些属性最容易混淆的是index和instance
    index 为同一级别组件的编号(也就是同一个父类的下的子类的序号不一定是同类)
    instance 针对的是整个页面的同一类控件的序号

这些属性可以通过uiautomationViewer来获取。关于如何获取就不再这里展开介绍了。

  • 使用文本来匹配控件
  • 使用描述来匹配控件
  • 使用类名来匹配控件
  • 使用包名来匹配控件
  • 使用资源ID来匹配控件
  • 使用其他属性来匹配控件
  • 使用index和instance来匹配控件
  • 我们每个页面View的层级关系中有如下几种关系:
    父节点
    子节点
    同胞节点
    先辈节点
    后辈节点
  • 使用index和instance来匹配控件
BySelector && By

BySelector 和 By是在UiAutomator2引入的是UiSelector的简化,但是BySelector的意义不大,一般个人角度我用得比较多的是By方式,十分简洁。
对应的匹配方法也和UiSelector有了一点改变,但是改变不是很多如果熟悉UiSelector可以不用任何学习的情况下切换过来。

clazz 设置类名称的条件匹配
desc 通过正则匹配设置一个描述搜索条件
descContains 通过包含匹配设置一个描述的搜索条件
descStartsWith 通过起始匹配设置一个描述的搜索条件
descEndsWith 通过结尾匹配设置一个描述的搜索条件
pkg通过包名正则匹配设置一个搜索条件
res 通过资源ID正则匹配设置一个搜索条件
text 通过文本正则匹配设置一个搜索条件
textContains 通过文本包含匹配设置一个搜索条件
textStartsWith 通过文本起始匹配设置一个搜索条件
textEndsWith 通过文本结尾匹配设置一个搜索条件
checkable
checked
clickable
enabled
focusable
focused
longClickable
scrollable
使用深度搜索来定位控件

我们知道View的层级结构是有一定层次的,我们在搜索的时候莪可以借助这个层次关系进行定位。

depth(int exactDepth) 设置搜索条件匹配元素,通过固定的层级深度
depth(int min, int max) 设置搜索条件匹配元素,通过一定范围的层级深度
maxDepth(int max) 设置一个搜索条件匹配元素,但是不能超过指定的深度
minDepth(int min) 设置一个搜索条件匹配元素,但是从指定深度开始向下搜索
hasChild(BySelector childSelector) 搜索子类
hasDescendant(BySelector descendantSelector) 搜索后代
hasDescendant(BySelector descendantSelector, int maxDepth) 搜索后代
UiObject && UiObject2

UiObject
代表一个组件对象,一般是通过UiSelector定位到的,在定位到的UiObject上可以执行一系列操作,UiObject2实在UiAutomator2中引入的接口方面差别不是很大。

  • 点击长按等操作
  • 拖拽/滑动操作
  • 文本操作
  • 获取属性/属性判断

  • 获取层级关系
  • 手势操作
  • 判断对象时候存在
UiCollection

UiCollection 代表元素集合
它首先按照一定的条件枚举出容器类界面所有符合条件的子元素,再在从符合条件的元素再次通过一定的条件最终定位需要的组件
UiCollection的接口比较少常见的Api如下:

public UiObject getChildByDescription(UiSelector childPattern, String text)
public UiObject getChildByInstance(UiSelector childPattern, int instance)
public UiObject getChildByText(UiSelector childPattern, String text)
public int getChildCount(UiSelector childPattern)
UiScrollable

UiScrollable专门处理滚动事件,提供各种滚动方法, 这个和Espresso的onData情形差不多都是用于测试不能完全显示的元素的控件。UiScrollable继承自UiCollection下面是对应的继承关系。

  • UiScrollable支持的操作
  • 快速滑动
  • 选择子项
  • 设置滑动参数
  • 向前向后滑动

  • 设置滑动方向
UiWatcher

在测试框架无法找到一个匹配时,使用Uiselector测试框架将自动调用此处理程序方法。在超时未找到匹配项时,框架调用checkforCondition()方法查找设备上的所有已注册的监听检查条件。我们可以使用此方法来处理中断问题保证测试用例正常运行。
监听器要在中断代码之前运行。
但是我个人而言还没实际在项目的测试用例中使用,主要原因是我们只能罗列优先的可能性,但是并不是所有的可能情况都列举出来,第二由于我们在测试的时候都是在理想状态下,所以一般都没写这部分逻辑,但是最好还是将这部分考虑在内。

UiDevice.getInstance().registerWatcher("answerThePhone",new UiWatcher() { 
UiObject ReceiveObject = new UiObject(new UiSelector().text("下拉接听"));
@Override
public boolean checkForCondition() {
// TODO Auto-generated method stub
System.out.println("监听器检查函数开始运行-挂电话");
if (jietingObject.exists()) {
System.out.println("监听器条件判断成功--挂电话");
int y = UiDevice.getInstance().getDisplayHeight();
int x = UiDevice.getInstance().getDisplayWidth();
UiDevice.getInstance().swipe(x / 2, y / 2, x / 2,10, 10);
return true;
}
System.out.println("监听器条件判断失败--挂电话");
return false;
}});
Until

我们在执行自动化测试的时候常常有一种需求就是需要等待某个属性达到某个条件的时候执行某个操作。要实现这个目的就必须借助于新引入的UiObject2的wait方法,

public <R> R wait(UiObject2Condition<R> condition, long timeout)

它会等待等到当前Object满足某个条件的时候返回一个boolean值用于表示条件是否满足。这里的UiObject2Condition就是借助Until来获取的,Until有很多静态方法可以很容易创建检测的条件。
下面是常见的检测条件:

public static UiObject2Condition<Boolean> checkable(final boolean isCheckable)
public static UiObject2Condition<Boolean> checked(final boolean isChecked)
public static UiObject2Condition<Boolean> clickable(final boolean isClickable)
public static UiObject2Condition<Boolean> enabled(final boolean isEnabled)
public static UiObject2Condition<Boolean> focusable(final boolean isFocusable)
public static UiObject2Condition<Boolean> focused(final boolean isFocused)
public static UiObject2Condition<Boolean> longClickable(final boolean isLongClickable)
public static UiObject2Condition<Boolean> scrollable(final boolean isScrollable)
public static UiObject2Condition<Boolean> selected(final boolean isSelected)
public static UiObject2Condition<Boolean> descMatches(final Pattern regex)
public static UiObject2Condition<Boolean> descMatches(String regex)
public static UiObject2Condition<Boolean> descEquals(String contentDescription)
public static UiObject2Condition<Boolean> descContains(String substring)
public static UiObject2Condition<Boolean> descStartsWith(String substring)
public static UiObject2Condition<Boolean> descEndsWith(String substring)
public static UiObject2Condition<Boolean> textMatches(final Pattern regex)
public static UiObject2Condition<Boolean> textMatches(String regex)
public static UiObject2Condition<Boolean> textNotEquals(final String text)
public static UiObject2Condition<Boolean> textEquals(String text)
public static UiObject2Condition<Boolean> textContains(String substring)
public static UiObject2Condition<Boolean> textStartsWith(String substring)
public static UiObject2Condition<Boolean> textEndsWith(String substring)
public static EventCondition<Boolean> newWindow()
public static EventCondition<Boolean> scrollFinished(final Direction direction)
public static SearchCondition<Boolean> gone(final BySelector selector) 某个Object消失
public static SearchCondition<Boolean> hasObject(final BySelector selector)
public static SearchCondition<UiObject2> findObject(final BySelector selector)
public static SearchCondition<List<UiObject2>> findObjects(final BySelector selector)

Configulator

主要用于配置一些参数

public Configurator setWaitForIdleTimeout(long timeout) 
public long getWaitForIdleTimeout()
public Configurator setWaitForSelectorTimeout(long timeout)
public long getWaitForSelectorTimeout()
public Configurator setScrollAcknowledgmentTimeout(long timeout)
public long getScrollAcknowledgmentTimeout()
public Configurator setActionAcknowledgmentTimeout(long timeout)
public long getActionAcknowledgmentTimeout()
public Configurator setKeyInjectionDelay(long delay)
public long getKeyInjectionDelay()

http://blog.csdn.net/swordgirl2011/article/category/3242309

Espresso 文档

Espresso 帮助文档

Espresso 环境搭建

在Android Studio 2.2中Expresso是默认引入的,你会发现在新创建的Android studio项目的build.gradle中已经帮我们添加好了如下的依赖,
并且已经帮我们创建好了androidTest目录,之前是没有这个待遇的,所以如果你还在为搭建环境困扰,不要纠结了升级你的Android Studio吧。

// Android JUnit Runner
androidTestCompile 'com.android.support.test:runner:0.5'
// JUnit4 Rules
androidTestCompile 'com.android.support.test:rules:0.5'
// Espresso core
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2'
// Espresso-web for WebView support
androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2.2'
// Espresso-idling-resource for synchronization with background jobs
androidTestCompile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'

当然如果你们公司不能上网那么只能下载如下的jar包存在对应的libs中:
自动化测试静态jar包

一定要注意需要添加 defaultConfig 节点添加

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

否则会遇到明明编写了测试用例,但是还是报没有测试项的错误:

Espresso 组成

Expresso 是由三大部分组成的:

ViewMachers:寻找用来测试的View
ViewActions:发送交互事件。
ViewAssertions:检验测试结果。

上述的操作可以简单归结为定位到某个控件,对某个空间执行某项操作,检查操作结束后的结果。

下面是Espresso的CheetSheet

ActivityTestRule

在开始自动化测试之前都会定义一个ActivityTestRule 它用于在测试的时候launch待测试的activity。Rules会在每个测试方法执行的时候执行,并且优先于 @Before方法。使用ActivityTestRule还能通过ActivityTestRule#getActivity()方法来获取某个activity的引用。

查找待操作的View

从上面的Espresso CheetSheet中可以看出查找视图可以用onView 和 onData两个方法,整体来说,onView()比较适用于UI比较简单的情况,一般我们获取这些简单的View可以通过id,text,Discription,Hint等属性来获取。但是需要注意的是上面的onView所找到的应该只能有一项,不能有多个View满足给出的条件,否则会报出AmbiguousViewMatcherException,如果遇到这种情况可以有两种方式一种是使用allof来添加多个条件来重新定位,另一个是找到具体的那个父View然后再定位。
但是,对于类似ListView, GridView, Spinner这种有UI复用的元素来说,,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,我们通过onView()是没有办法找到的。onView就不能胜任了只能借助onData来完成。

我们首先对比下onView和onData两个方法:

从返回值角度来看:

  • onView返回的是ViewInteraction,它是一个符合匹配条件的唯一目标控件
  • onData返回的是DataInteraction,它关注于数据。
    从传入参数角度来看:
  • onView 传入的是Matcher 它是一个View匹配的匹配规则
  • onData 传入的是Matcher<? extends Object> 它是针对数据匹配的匹配规则

也就是说onView偏向于直接匹配视图,而onData由于有些视图不能完全显示所以偏向于从数据入手。也就是Adapter对应的Data入手

在AdapterView中点击某项的例子:

先上代码:

public class ItemMatcher {
public static Matcher<Object> withName(final String name) {
return new BoundedMatcher<Object,ItemInfo>(ItemInfo.class) {
@Override
public void describeTo(Description description) {
description.appendText("item mathcher");
}

@Override
protected boolean matchesSafely(ItemInfo item) {
if(item != null && !TextUtils.isEmpty(item.getName()) && item.getName().equals(name)) {
return true;
}
return false;
}
};
}
}

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {

@Rule
public ActivityTestRule<MainActivity> mActityTestRule = new ActivityTestRule<MainActivity>(MainActivity.class);

@Test
public void testScroller() {
onData(allOf(instanceOf(ItemInfo.class), ItemMatcher.withName("Jimmy 999")))
.inAdapterView(withId(R.id.listviews))
.onChildView(withId(R.id.name))
.perform(click());
}
}

在上面我们使用了onData来在数据集中查找,这里最重要的就是自定义的ItemMatcher,通过它可以找到name为”Jimmy 999”的那一项,在有多个ListView的时候可以使用inAdapterView来指定要查找的listView。找到之后会自动滚动到哪一项,执行点击操作。

public void testClickSpecialItem() {
onData(instanceOf(ItemInfo.class))
.inAdapterView(allOf(withId(R.id.listviews), isDisplayed()))
.atPosition(99)
.onChildView(withId(R.id.name))
.perform(click());
}

当然并不是所有的AdapterView都像上面那么复杂比如点击某项Spinner可以使用如下方式:

onData(allOf(is(instanceOf(String.class)),is("xxxx"))).perform(click());

在googlesample中给出的一个测试SimpleAdapter的例子如下:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class TestSimpleAdapter {
private static final String TEXT_ITEM_50 = "item: 95";
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void testCheckBoxClick() {
onRow(TEXT_ITEM_50).onChildView(withId(R.id.checkbox)).perform(click());
onRow(TEXT_ITEM_50).onChildView(withId(R.id.checkbox)).check(matches(isChecked()));
}

public static DataInteraction onRow(String str) {
return onData(hasEntry(is(MainActivity.ROW_TEXT1),is(str)));
}
}
RecyclerView 的用法:

当要测试RecyclerView的时候需要添加如下依赖:

// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2'

这个不是默认添加的所以需要自己添加。并且这个库的某些依赖和support有冲突需要使用移除部分,也可以考虑上面说的离线的方式添加对应的jar包。

@Test
public void testrecycleView() {

onView(withId(R.id.recycleviews)).perform(RecyclerViewActions.actionOnItemAtPosition(909, click()));

onView(withId(R.id.recycleviews)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("Name
999")), click()));

onView(withId(R.id.recycleviews)).perform(RecyclerViewActions.actionOnHolderItem(new
CustomViewHolderMatcher(hasDescendant(withText("Name 999"))), click()));

}

private static class CustomViewHolderMatcher extends TypeSafeMatcher<RecyclerView.ViewHolder> {
public CustomViewHolderMatcher() {
}
public CustomViewHolderMatcher(Matcher<View> viewMatcher) {
itemMatcher = viewMatcher;
}
private Matcher<View> itemMatcher = any(View.class);
@Override
protected boolean matchesSafely(RecyclerView.ViewHolder viewHolder) {
return MainActivity2.ItemBeanAdapter.MyViewHolder.class.isAssignableFrom(viewHolder.getClass())
&& itemMatcher.matches(viewHolder.itemView);
}
@Override
public void describeTo(Description description) {
description.appendText("this is assigned from view holder");
}
}
hasSibling的用法

Sibling 的意思是 兄弟,姐妹;[生]同科,同属;[人]氏族成员
比如我们有一个ListView每一项都有一个显示“70”的TextView,这些TextView的text属性和id都是相同的,但是每一个TextView左边都放置有不同的TextView。如下图所示,此时可以通过如下的代码出目标项:

onView(allOf(withText("70"), hasSibling(withText("Jimmy: 12"))))
.perform(click());

这里需要注意的是这些项必须是在屏幕上可见的。

inRoot 验证Toast && AutoComplete

这个很简单就是判断Toast时候在Root之上。

onView(withText("Position = 99"))
.inRoot(withDecorView(not(mActityTestRule.getActivity().getWindow().getDecorView())))
.check(matches(isDisplayed()));

自定义ViewAction

一般我们要执行某个操作都是针对某个匹配的View的所以我们一般作为参数的都是Matcher,里面使用Matcher找到空间并添加ViewAction 在perform方法中会传入对应的View可以根据这个传入的View对其进行操作
下面是两个自定义的ViewAction:getText 会从View中获取文本,changeTextColor会改变文本的颜色。

public String getText (Matcher<View> matcher) {
final String[] textStr = {null};
onView(matcher).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(TextView.class);
}
@Override
public String getDescription() {
return "get text from TextView";
}
@Override
public void perform(UiController uiController, View view) {
textStr[0] = ((TextView) view).getText().toString();
}
});
return textStr[0];
}
public void changeTextColor(Matcher<View> matcher) {
onView(matcher).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(TextView.class);
}
@Override
public String getDescription() {
return "Change text Color";
}
@Override
public void perform(UiController uiController, View view) {
((TextView)view).setTextColor(Color.parseColor("#ff0000"));
}
});
}

要输入中文可以使用如下的方式:
onView(withId(R.id.imputString)).perform(replaceText(“各种格式文件”));
对于一般的View我们可以使用onView来定位,但是对于AdapterView(ListView GridView Spinner都是属于AdapterView)就需要onData来进行定位。

自定义Idling Resource

Idling Resource 用于需要等待异步计算或I/O操作完成的情况。使用它Espresso会等待app处于idle状态,才会执行下个动作和检查下个断言。

为了实现IdlingResource,需要重写3个函数:getName(),registerIdleTransitionCallback(),isIdleNow()。

* getName():必须返回代表idling resource的非空字符串,这个一般个人不做过多的纠结直接通过XXXX.class.getName()
* isIdleNow():返回当前idlingresource的idle状态。
* registerIdleTransitionCallback(IdlingResource.ResourceCallback callback) 用于注入回调。

下面是一个在网上找的比较典型的例子,就是主程序使用IntentService处理一个耗时操作,等到耗时操作结束的时候进行后续的测试

@Override
public String getName() {
return IntentServiceIdlingResource.class.getName();
}

@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}

@Override
public boolean isIdleNow() {
boolean idle = !isIntentServiceRunning();
if (idle && resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
return idle;
}

private boolean isIntentServiceRunning() {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) {
if (RepeatService.class.getName().equals(info.service.getClassName())) {
return true;
}
}
return false;
}
注册和注销idling resource

为了让Espresso等待自定义的idling resource,你需要注册它。在测试代码的@Before方法中执行注册,在@After中执行注销。

@Before
public void registerIntentServiceIdlingResource() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
idlingResource = new IntentServiceIdlingResource(instrumentation.getTargetContext());
registerIdlingResources(idlingResource);
}
@After
public void unregisterIntentServiceIdlingResource() {
unregisterIdlingResources(idlingResource);
}

Espresso Intent 测试

要测试Intent就必须使用到如下的库:

androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2'

这个库在上面介绍的离线库中也可以找到。

在使用Intent进行测试的时候需要首先明确两个方法:
intending and intended
intending 相当于 when ,respondWith 相当于 thenReturn。intending还有个用法是用于检查返回的Intent携带的参数
intended 用于验证某个Intent是否被发送。
在使用这两个方法的时候必须先初始化Intents。用完后必须调用release释放。

  • 检测某个Intent是否发送成功
private Matcher<Intent> expectedIntent;
@Rule
public final ActivityTestRule<MainActivity> rule =
new ActivityTestRule<>(MainActivity.class);
@Before
public void setupIntents() {
expectedIntent = allOf(toPackage(InstrumentationRegistry.getTargetContext().getPackageName()),isInternal());
}
@Test
public void testLoginIntent() {
Intents.init();
onView(withId(R.id.button_login)).perform(click());
intended(expectedIntent);
Intents.release();
}
  • 检测某个Intent 发送后的响应是否正确:

有的时候我们会按下某个按钮发送一个Intent来获取某个结果,这个结果一般在onActivityResult中返回。如果要测试返回某个结果时候的情况可以使用如下的代码来验证

public void testLoginPass() {
//创建一个响应的ActivityResult
Instrumentation.ActivityResult activityResult = new Instrumentation.ActivityResult(
Activity.RESULT_OK, new Intent());
//Intent初始化
Intents.init();
//将activityResult与待发送的expectedIntent绑定
intending(expectedIntent).respondWith(activityResult);
//执行操作
onView(withId(R.id.button_login)).perform(click());
//验证是否发送成功
intended(expectedIntent);
//释放Intent
Intents.release();
//检验返回activityResult时的现象是否正确
onView(withId(R.id.button_login)).check(matches(withText(R.string.pass_login)));
}

  • 验证发送出去的数据
    Intent 一般会携带一些数据我们可以通过hasExtra来检查是否发送带有特定键值的数据,

    @Test
    public void testLoginSuccess() throws InterruptedException {

    onView(withId(R.id.email)).perform(typeText(TEST_USER));
    onView(withId(R.id.password)).perform(typeText(TEST_PASS));
    closeSoftKeyboard();
    Intents.init();
    onView(withId(R.id.email_sign_in_button)).perform(click());
    //等待20
    TimeUnit.SECONDS.sleep(20);
    //检验返回的数据
    intending(hasExtra(MainActivity.LOGIN_NAME, TEST_USER));
    Intents.release();
    }

    Espresso中提供了许多方法用于检测Intent的各个部分,下面是每个字段的对应关系

    Intent.setData <–> hasData
    Intent.setAction <–> hasAction
    Intent.setFlag <–> hasFlag
    Intent.setComponent <–> hasComponent
  • 发送一个带有数据的Intent检验返回的结果

    @Before
    public void setupImageUri() {

    Resources resources = InstrumentationRegistry.getTargetContext().getResources();
    Uri imageUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + resources
    .getResourcePackageName(
    R.mipmap.ic_launcher) + '/' + resources.getResourceTypeName(
    R.mipmap.ic_launcher) + '/' + resources.getResourceEntryName(
    R.mipmap.ic_launcher));
    Intent resultData = new Intent();
    resultData.setData(imageUri);
    //创建一个Intent响应结果
    mActivityResult = new Instrumentation.ActivityResult(
    Activity.RESULT_OK, resultData);
    }

    @Test
    public void testSelectImage() {

    //Check the image is not displayed
    onView(withId(R.id.imageView)).check(matches(not(hasDrawable())));
    //Setup the intent
    Intents.init();
    //将响应结果绑定到具有如下条件的Intent上
    Matcher<Intent> expectedIntent = allOf(hasAction(Intent.ACTION_PICK),
    hasData(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI));
    intending(expectedIntent).respondWith(mActivityResult);

    //执行操作触发满足上述条件的Intent
    onView(withId(R.id.fab_image)).perform(click());
    //检验是否发送成功
    intended(expectedIntent);
    Intents.release();

    //Check the image is displayed
    onView(withId(R.id.imageView)).check(matches(hasDrawable()));

    }
  • 测试启动Activity的Intent:
    首先要设置不启动Activity

    @Rule
    public final ActivityTestRule<TextHashActivity> rule =
    new ActivityTestRule<>(TextHashActivity.class, false,
    false); // not launch the activity
    @Test
    public void testCorrectIntent() throws InterruptedException {
    //设置启动Activity的Intent
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_SEND);
    intent.setType(TextHashActivity.SUPPORT_TYPE);
    intent.putExtra(Intent.EXTRA_TEXT, TEST_INPUT);
    //启动Activity
    rule.launchActivity(intent);

    TimeUnit.SECONDS.sleep(1);
    onView(withId(R.id.textview_sha1))
    .check(matches(withText(TEST_SHA1)));
    }

    另一种方式测试:

@Test
public void testFromOtherApp() throws InterruptedException {

Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setType(TextHashActivity.SUPPORT_TYPE);
intent.putExtra(Intent.EXTRA_TEXT, TEST_INPUT);

// There maybe more than one app will handle this requirement
String packageName = InstrumentationRegistry.getTargetContext().getPackageName();
ComponentName componentName = new ComponentName(packageName,
TextHashActivity.class.getName());
intent.setComponent(componentName);

Intents.init();
InstrumentationRegistry.getContext().startActivity(intent);
Matcher<Intent> expectedIntent = hasComponent(componentName);
intended(expectedIntent);
Intents.release();

TimeUnit.SECONDS.sleep(1);
onView(withId(R.id.textview_sha1))
.check(matches(withText(TEST_SHA1)));
}
  • 想让每个Activity启动的时候都收到某个Intent

    @SmallTest
    @RunWith(AndroidJUnit4.class)
    public class MainActivityTest {
    @Rule
    public ActivityTestRule<MainActivity> mActivityRule =
    new ActivityTestRule<MainActivity>(MainActivity.class) {
    @Override
    protected Intent getActivityIntent() {
    Context targetContext = InstrumentationRegistry.getInstrumentation()
    .getTargetContext();
    Intent result = new Intent(targetContext, MainActivity.class);
    result.putExtra("Name", "Value");
    return result;
    }
    };

    }
  • 排除外部Intent的影响

intending(not(isInternal())).(new ActivityResult(Activity.RESULT_OK, null));

Espresso 测试用例中使用源码中的数据

要想在测试用例中使用源码中的数据可以使用VisibleForTesting 这个注释符。

@VisibleForTesting
public static final String VALID_ENDING = "coffee";

参考文章

http://michaelevans.org/blog/2015/09/15/testing-intents-with-espresso-intents/
https://github.com/pengj/Intent-Test
http://collectiveidea.com/blog/archives/2015/06/11/testing-for-android-intents-using-espresso/
https://segmentfault.com/a/1190000004338384
guides.codepath.com/android/UI-Testing-with-Espresso
http://www.jianshu.com/p/ad8b3514b852
https://segmentfault.com/a/1190000004355178
http://blog.csdn.net/qq744746842/article/details/51005604
http://stackoverflow.com/questions/32142463/how-to-stub-select-images-intent-using-espresso-intents
http://pengj.me/android/test/2015/10/17/expresso-test-intent.html

Monkey主要是用来对软件的稳定性与压力测试的工具。它会模拟用户向手机发送伪随机事件。

保存Monkey结果
保存在PC中    adb shell monkey [option] <count> > /local/monkey.txt
保存在手机中 monkey [optinon] <count> > /mnt/sdcard/monkey.txt
标准流与错误流分开保存 monkey [optinon] <count> 1> /mnt/sdcard/monkey.txt 2> /mnt/sdcard/error.txt
Monkey测试参数
Monkey向命令行打印输出的log信息级别
默认级别0:-v 只打印启动信息,测试完成信息和最终结果信息
级别1:-v -v 打印执行时的一些信息,如发送事件
级别2:-v -v -v 打印最详细信息
-p <允许执行的包名列表>
-c <意图的种类> 主要用于测试没有桌面图标的应用 -c 后面添加catory
-s <随机数种子> 如果用相同的seed值再次运行Monkey,它将生成相同的事件序列
--throttle <毫秒> 在事件之间插入固定延迟。通过这个选项可以减缓Monkey的执行速度。

--pct-touch <percent>调整触摸事件的百分比
--pct-motion <percent>调整动作事件的百分比
--pct-trackball <percent> 调整轨迹事件的百分比
--pct-nav <percent> 调整“基本”导航事件的百分比
--pct-majornav <percent> 调整“主要”导航事件的百分比
--pct-syskeys <percent>调整“系统”按键事件的百分比
--pct-appswitch <percent>调整启动Activity的百分比。
--pct-anyevent <percent> 调整其它类型事件的百分比。


--hprof 设置此选项,将在Monkey事件序列之前和之后立即生成profiling报告。这将会在data/misc中生成大文件(~5Mb)
--ignore-crashes 如果设置此选项,当发生崩溃的时候Monkey将继续向系统发送事件,直到计数完成
--ignore-timeouts 如果设置此选项,当发生AMR的时候Monkey将继续向系统发送事件,直到计数完成
--ignore-security-exceptions如果设置了此选项,当发生权限问题的时候Monkey将继续向系统发送事件,直到计数完成
--ignore-native-crashes 当发生C++/C层发生错误的时候Monkey将继续向系统发送事件,直到计数完成

白名单和黑名单要push到手机中的/data/local/tmp
--pkg-blacklist-file PACKAGE_BLACKLIST_FILE(文件路径) 添加到黑名,位于黑名单中的引用将不进行测试
--pkg-whitelist-file PACKAGE_WHITELIST_FILE(文件路径) 添加到黑名,位于黑名单中的引用将不进行测试
如何组合测试参数

上面介绍了很多的参数,大家并不需要记住,需要用的时候直接使用adb shell monkey -help即可输出,但是还是要牢记这些参数的意义的,参数这么多我们要怎么确定这些参数呢?
一般我们会根据我们需要测试的目的进行对测试事件百分比进行分配。并且还要结合要复现的问题进行忽略设计,有时候还得考虑某些应用如果随意测试会产生某些不良影响的时候还需要考虑到把这些应用添加到黑名单中。
总之这些参数要怎么设置需要结合实际需求来决定。

常见事件
事件0 --pct-touch
:Sending Touch (ACTION_DOWN): 0:(299.0,255.0)
:Sending Touch (ACTION_UP): 0:(302.0262,250.57063)
事件1 --pct-motion
:Sending Touch (ACTION_DOWN): 0:(328.0,220.0)
:Sending Touch (ACTION_MOVE): 0:(317.66824,217.7649)
:Sending Touch (ACTION_MOVE): 0:(315.09308,217.11836)
:Sending Touch (ACTION_MOVE): 0:(304.76135,214.29372)
:Sending Touch (ACTION_UP): 0:(291.04208,211.98477)
事件3 --pct-trackball
:Sending Trackball (ACTION_MOVE): 0:(0.0,1.0)
:Sending Trackball (ACTION_MOVE): 0:(-1.0,-4.0)
:Sending Trackball (ACTION_MOVE): 0:(1.0,1.0)
事件4--pct-pinchzoom
:Sending Touch (ACTION_DOWN): 0:(487.0,209.0)
:Sending Touch (ACTION_POINTER_DOWN 1): 0:(486.9106,205.45831) 1:(194.0,366.0)
:Sending Touch (ACTION_MOVE): 0:(484.5785,183.66449) 1:(206.95853,375.98056)
:Sending Touch (ACTION_MOVE): 0:(471.26563,161.93521) 1:(218.35042,380.1656)
:Sending Touch (ACTION_MOVE): 0:(457.90872,153.87688) 1:(222.07701,383.26407)
:Sending Touch (ACTION_MOVE): 0:(452.51602,142.13242) 1:(242.29489,384.48602)
:Sending Touch (ACTION_MOVE): 0:(444.63513,124.26505) 1:(255.93825,391.4393)
:Sending Touch (ACTION_MOVE): 0:(444.01697,120.63037) 1:(273.75214,395.4244)
:Sending Touch (ACTION_MOVE): 0:(434.89807,117.38953) 1:(294.31616,402.74707)
:Sending Touch (ACTION_POINTER_UP 1): 0:(428.31845,98.71772) 1:(294.42966,402.80002)
:Sending Touch (ACTION_UP): 0:(415.8634,86.58714)
事件5 --pct-nav
:Sending Key (ACTION_UP): 19 // KEYCODE_DPAD_UP
:Sending Key (ACTION_DOWN): 20 // KEYCODE_DPAD_DOWN
:Sending Key (ACTION_UP): 21 // KEYCODE_DPAD_LEFT
:Sending Key (ACTION_DOWN): 22 // KEYCODE_DPAD_RIGHT
事件6 --pct-majornav
:Sending Key (ACTION_DOWN): 82 // KEYCODE_MENU
:Sending Key (ACTION_DOWN): 23 // KEYCODE_DPAD_CENTER
事件7 --pct-syskeys
:Sending Key (ACTION_DOWN): 4 // KEYCODE_BACK
:Sending Key (ACTION_DOWN): 5 // KEYCODE_CALL
:Sending Key (ACTION_DOWN): 25 // KEYCODE_VOLUME_DOWN
:Sending Key (ACTION_DOWN): 24 // KEYCODE_VOLUME_UP
:Sending Key (ACTION_UP): 3 // KEYCODE_HOME
事件8 --pct-appswitch
:Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.android.calendar/.AllInOneActivity;end
// Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.android.calendar/.AllInOneActivity } in package com.android.calendar
事件9 --pct-flip
:Sending Flip keyboardOpen=true
:Sending Flip keyboardOpen=false
事件10 --pct-anyevent
:Sending Key (ACTION_DOWN): 128 // KEYCODE_MEDIA_CLOSE
:Sending Key (ACTION_UP): 128 // KEYCODE_MEDIA_CLOSE
:Sending Key (ACTION_DOWN): 57 // KEYCODE_ALT_LEFT
:Sending Key (ACTION_UP): 57 // KEYCODE_ALT_LEFT
:Sending Key (ACTION_DOWN): 9 // KEYCODE_2
:Sending Key (ACTION_UP): 9 // KEYCODE_2
:Sending Key (ACTION_DOWN): 61 // KEYCODE_TAB
:Sending Key (ACTION_UP): 61 // KEYCODE_TAB
事件11:--pct-rotation 屏幕旋转百分比 隐藏事件
:Sending rotation degree=0, persist=true
:Sending rotation degree=1, persist=false
:Sending rotation degree=2, persist=true
:Sending rotation degree=3, persist=false

延时事件:
Sleeping for 0 milliseconds
事件结果报告
// Seeded: 1435740661667         随机种子
// Event percentages:
// 0: 16.0% 事件0:--pct-touch
// 1: 9.0% 事件1:--pct-motion
// 2: 2.0% 事件2:--pct-pinchzoom
// 3: 15.0% 事件3:--pct-trackball
// 4: -0.0% 事件4:--pct-rotation
// 5: 25.0% 事件5: --pct-nav
// 6: 15.0% 事件6:--pct-majornav
// 7: 2.0% 事件7:--pct-syskeys
// 8: 2.0% 事件8: --pct-appswitch
// 9: 1.0% 事件9:--pct-flip
// 10: 13.0% 事件10:--pct-anyevent

所有事件跑完结束:
Events injected: 100
:Sending rotation degree=0, persist=false
:Dropped: keys=0 pointers=0 trackballs=0 flips=0 rotations=0
## Network stats: elapsed time=270ms (0ms mobile, 0ms wifi, 270ms not connected)
// Monkey finished

遇到异常结束:
** Monkey aborted due to error.
Events injected: 1744
:Sending rotation degree=0, persist=false
:Dropped: keys=3 pointers=8 trackballs=0 flips=0 rotations=0
## Network stats: elapsed time=55269ms (0ms mobile, 0ms wifi, 55269ms not connected)
** System appears to have crashed at event 1744 of 10000 using seed 1435753466327

自动化测试的重要性做Android开发的开发人员应该都了解吧,在开发过程中我们需要实时对我们已有的代码进行测试,由于是会相互影响的,有时候我们开发新功能的时候会发现原先是OK的部分,却在交付的时候出现了异常,所以一些稍稍正规点的公司都会要求在开发过程中对重要的部分编写测试用例进行自动化测试,这些用例就像哨兵一样看着应用的每个功能和每段逻辑,这个在敏捷开发中显得尤为重要。在提交了代码就让它自动执行自动化测试用例,一旦测试不通过就意味着你的代码影响了某个功能点。还有一种情况是有时候测试工程师报出的问题是偶现的,几百次可能还不会出现一次,在Log中分析到了可能的原因,你修改后不知道是否真正解决了这个问题,那要怎么办?自己手动验证?叫测试帮忙验证?这些都不大现实吧,这时候就要借助自动化测试工具来进行测试了。写个用例让它没日没夜得跑,跑它个几百次看下是否还有异常。
随着Android Studio的功能不断完善,对自动化测试的支持也不断加强,最早的时候在Android studio上搭建个自动化测试就得耗费一整天,某些自动化测试框架由于使用说明很少会给搭建带来更大的困难。借下来的部分将会以Android Studio 2.2为开发环境介绍自动化测试平台的搭建。这里主要介绍Expresso,UiAutomater,Roblolectric,JUnit这四个主流的自动化测试框架,其中前两者是UI测试框架,一般用于功能测试,后两者是单元测试框架,一般用于逻辑测试。这四者各有各的优点,也各有各的不足。

依赖关系

在Android Stdio 2.2 Preview 版本中一旦创建了一个项目就会自动导入Junit4以及Expresso的依赖如下所示:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha1'
testCompile 'junit:junit:4.12'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support:support-annotations:23.3.0'
}

并且帮你创建好了功能测试和单元测试的目录如下所示:

下面我们就以Calculater这个类作为待测试的类看下逻辑用例如何创建:

  1. 创建一个待测试类:

    public class Calculater {

    public int add(int a,int b) {
    }

    public int sub(int a,int b) {
    }

    public int mul(int a,int b) {
    }
    }
  2. 按下Ctrl + Shint + T选择Create New Test…
    在弹出的对话框中编辑测试类的类名,勾选要进行测试的方法,以及是否创建setup/tearDown方法

  3. 紧接着选择要放置的路径,这里选择的是test目录:

  4. 这时候就会自动生成如下的代码

    public class CalculaterTest {
    @Before
    public void setUp() throws Exception {

    }

    @After
    public void tearDown() throws Exception {

    }

    @Test
    public void add() throws Exception {

    }

    @Test
    public void sub() throws Exception {

    }

    @Test
    public void mul() throws Exception {

    }

    }
  5. 在对应的方法的测试方法中编写对应的测试:

    public class CalculaterTest {

    private Calculater mCalculater;
    @Before
    public void setUp() throws Exception {
    mCalculater = new Calculater();
    }

    @After
    public void tearDown() throws Exception {
    mCalculater = null;
    }

    @Test
    public void add() throws Exception {
    assertNotNull(mCalculater);
    assertEquals(5,mCalculater.add(2,3));
    }

    @Test
    public void sub() throws Exception {
    assertNotNull(mCalculater);
    assertEquals(5,mCalculater.sub(12,7));
    }

    @Test
    public void mul() throws Exception {
    assertNotNull(mCalculater);
    assertEquals(6,mCalculater.mul(2,3));
    }
    }
  6. 有了上面的测试类就可以编写待测试的类了,比如如下所示

    public class Calculater {

    public int add(int a,int b) {
    return a + b;
    }

    public int sub(int a,int b) {
    return a - b;
    }

    public int mul(int a,int b) {
    return a * b;
    }
    }
  7. 点击如下所示左边的绿色箭头就可以运行整个测试类中的测试用例

运行结果如下:

  1. 点击每个方法左边的绿色标志就可以触发单个方法的测试

运行结果如下

AndroidJunitRunner

AndroidJUnitRunner
用于运行Junit3 Junit4的测试运行器,用于替换InstrumentationTestRunner这个比较旧的运行器,它对其进行了较大的扩展使得Espresso和UiautoMator能够完美配合在一起。如果要运行Jnuit4则需要添加@Runwith
同时可以添加如下注释来控制测试运行:

@RequiresDevice来指定该条测试只运行在物理设备,而不是模拟设备
@SdkSupress(miniSdkVersion=18) 限制在指定的Android设备上运行
@MediumTest @LargeTest @SmallTest 用于将测试用例按照重要程度进行分类,在进行测试的时候可以选择只运行某个类别的测试用例。

同时AndroidJunitRunner支持Jnit4注释:

@Test 指定当前是一个测试方法
@Before 每个测试之前都会被执行
@After 每个测试方法运行结束之后都会被执行
@BeforeClass 在这个方法所在测试类运行之前会被执行,并且在一个类中只会执行一次
@AfterClass 在这个方法所在测试类运行结束之后会被执行,并且在一个类中也只会执行一次
@Ignore 该方法被忽略

运行AndroidJunitRunner的时候如果需要获取测试Apk和待测试Apk的资源的时候就要借助于InstrumentationRegistry.它有如下对象:

Instrmentation对象,UiAutomator初始化的时候就需要用到这个对象。
目标App的Context
测试App的COntext
传入的命令参数