Android给我们提供了一个完整的测试框架,使得我们可以从不同的层次对应用进行全方位的测试,包括单元测试,框架测试、ui自动化测试等等。Android测试架构如图所示:
转载请注明出处: http://blog.csdn.net/qinjunni2014/article/details/45854305
所有的Android测试都是基于JUnit,我们完全可以构建不调用Android api的测试,比如下面这样:
import junit.framework.TestCase;
/** * Created by Junli on 5/19/15. */
public class UnitTest extends TestCase {
public void test() throws Exception{
final int expected = 1;
final int reality = 5;
assertEquals(expected, reality);
}
}
我们只要继承自TestCase就可以构建一个基于JUnit的单元测试,只要是以test开头的函数都会被加入测试队列。在测试时,我们使用Assert库中的函数,assertEquals进行相等测试。但是,这段测试只能在device上进行测试,没法本地测试,如果尝试进行本地测试,就会抛出Stub! Not implemented” exception。实际上,Android中所有的测试都只能在device上跑,除了某些第三方测试框架比如Robolectric。
通常情况下,Android 应用中所有的组件的生命周期都是系统决定的,比如Activity的onCreate, onResume, onPause等方法的调用都是系统内部决定的。开发者无法通过系统api来直接调用,但是通过Instrumentation我们却可以做到这一点。而且在Instrumentation中,我们还可以向应用发送事件。
在测试Activity时,我们需要继承自ActivityUnitTestCase,我们首先写一个简单的Activity
public class MainActivity extends Activity {
EditText edit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
}
public void onClick(View v){
Intent intent = new Intent(this, SecondActivity.class);
intent.putExtra("text", edit.getText());
startActivity(intent);
}
}
布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="Start"
android:layout_centerInParent="true"
android:id="@+id/btn"/>
<EditText
android:layout_width="250dp"
android:layout_height="50dp"
android:id="@+id/edit"
android:layout_marginTop="15dp"
android:layout_centerHorizontal="true"
android:layout_below="@id/btn"/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/btn"
android:layout_marginTop="10dp"/>
</RelativeLayout>
布局文件包括一个button,一个EditText,一个TextView。我们来进行一些简单地测试
public class MainActivityUnitTest extends ActivityUnitTestCase<MainActivity> {
private int buttonId;
private MainActivity activity;
public MainActivityUnitTest() {
super(MainActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
Intent intent = new Intent(getInstrumentation().getTargetContext(), MainActivity.class);
startActivity(intent, null, null);
activity = getActivity();
}
public void testLayout() {
buttonId = R.id.btn;
assertNotNull(activity.findViewById(buttonId));
Button view = (Button) activity.findViewById(buttonId);
assertEquals("Incorrect label of the button", "Start", view.getText());
}
}
我们在setup函数中做一些简单的测试。我们首先要启动我们的MainActivity,然后通过getActivity方法拿到Activity的引用。在testLayout函数中,我们会先确定通过Activity的findViewById方法能拿到view,通过assertNotNull方法测试。然后调用assertEquals函数确定button的text是否正确。
ActivityUnitTestCase允许我们去检查activity的布局,检查启动其他activity时的intent,这个intent并不会被发送给Android系统,不过我们可以调用getStartedActivityIntent拿到它,并对它进行测试。ActivityUnitTestCase 中所有的测试方法都实在单独的Context中执行的,因此我们必须在setup中主动调用startActivity。
public void testIntent()
{
buttonId = R.id.btn;
assertNotNull(activity.findViewById(buttonId));
Button view = (Button) activity.findViewById(buttonId);
view.performClick();
Intent intent = getStartedActivityIntent();
assertNotNull(intent);//测试intent是否null
String data = intent.getExtras().getString("URL");
//测试URL是否正确
assertEquals("http://blog.csdn.net/qinjunni2014", data);
}
受限于ActivityUnitTestCase的功能,我们并不能在其中测试特别复杂的功能,比如与其他应用组件交互,发送事件处理等等。不过如果我们要实现这些功能的测试,只需要继承ActivityInstrumentationTestCase2即可,让我们通过实例来看看,这个类可以让我们做哪些测试。
//我们依然是对上面的MainActivity进行测试
public class MainActivityFunctionalTest extends ActivityInstrumentationTestCase2<MainActivity> {
private MainActivity activity;
private EditText editText;
private TextView textView;
public MainActivityFunctionalTest() {
super(MainActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
setActivityInitialTouchMode(true);
activity = getActivity();
editText = (EditText) activity.findViewById(R.id.edit);
textView = (TextView) activity.findViewById(R.id.text);
}
public void testStartSecondActivity(){
Instrumentation.ActivityMonitor monitor =
getInstrumentation().addMonitor(SecondActivity.class.getName(),null,false);
Button view = (Button) activity.findViewById(R.id.btn);
TouchUtils.clickView(this, view);
SecondActivity startedActivity = (SecondActivity) monitor.waitForActivityWithTimeout(2000);
assertNotNull("ReceiverActivity is null", startedActivity);
assertEquals("Monitor for ReceiverActivity has not been called",
1, monitor.getHits());
TextView textView = (TextView) startedActivity.findViewById(R.id.resultText);
ViewAsserts.assertOnScreen(startedActivity.getWindow().getDecorView(), textView);
assertEquals("Text incorrect", "Started", textView.getText().toString());
this.sendKeys(KeyEvent.KEYCODE_BACK);
TouchUtils.clickView(this, view);
}
}
注意到我们在setup函数中,并没有主动去调用startActivity。因为和UnitTestCase不同的是,这个类会利用真正的Android系统架构,在getActivity中会去生成这个activity。
testStartSecondActivity函数主要是去测试启动SecondActivity的过程,这里用到了ActivityMonitor,它会对某个类型的Intent进行监测,当有activity启动时,会去检查monitor,如果intent匹配,就会将monitor的hit count增加。通过TouchUtis我们可以对view进行点击操作。monitor.waitForActivityWithTimeout会等待2000ms,直到超时或者符合monitor监测的目标activity已经被启动。ViewAsserts.assertOnScreen可以测试我们的view是否在屏幕上,不过我们要将我们activity的DecorView作为root传进去。我们还可以通过调用sendKeys向Activity传递keyEvent事件。
我们以一个例子进行分析。
public class AsyncTaskActivity extends Activity implements View.OnClickListener {
Button button;
private IJobListener listener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
button = (Button) findViewById(R.id.btn);
button.setOnClickListener(this);
}
public static interface IJobListener {
void executionDone();
}
public void setListener(IJobListener listener) {
this.listener = listener;
}
@Override
public void onClick(View v) {
if(v == button){
myTask.execute();
}
}
final AsyncTask<String, Void, String> myTask = new AsyncTask<String, Void, String>() {
@Override
protected String doInBackground(String... arg0) {
return "Long running stuff";
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (listener != null) {
listener.executionDone();
}
}
};
}
在点击button的时候,会执行一个AsyncTask,任务执行完毕的时候。回去调用listener的executionDone,我们如何测试这个任务执行完了呢?
public class AsyncTaskTest extends ActivityUnitTestCase<AsyncTaskActivity> {
AsyncTaskActivity activity;
Button btn;
public AsyncTaskTest() {
super(AsyncTaskActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
Intent intent = new Intent(getInstrumentation().getTargetContext(),
AsyncTaskActivity.class);
startActivity(intent,null, null);
activity = getActivity();
btn = (Button) activity.findViewById(R.id.btn);
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
public void testAsyncTask() throws Throwable
{
//初始化一个count为1的CountDownLatch
final CountDownLatch latch = new CountDownLatch(1);
//任务执行完时,将latch的count减一
activity.setListener(new AsyncTaskActivity.IJobListener() {
@Override
public void executionDone() {
latch.countDown();
}
});
//点击button,启动任务
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
btn.performClick();
}
});
//阻塞等待任务执行完毕,或者超时
boolean await = latch.await(30, TimeUnit.SECONDS);
assertTrue(await);
}
}
这样我们就可以通过latch来判断异步任务是否在指定时间内执行完毕。
Monkey是一只可爱的猴子,我们可以让它为我们模仿用户的点击事件,随机生成一定数量的事件流,比如点击、滑动等其他手势操作。基本用法为:
$ adb shell monkey [options] < event-count >
Monkey提供了很多选项,但大致可以分为四类:
比如我们想,对com.junli.test这个app进行测试,事件数量为1000, 输出详细信息,包括activity的切换;而且不执行那些系统级别的controlkey事件,比如home ,back , volume control,我们可以这么做
adb shell monkey -p com.junli.test -v 1500 –pct-syskeys 1 1000
-p指定包名,-v 指定输出信息级别,-pct-syskeys 指定系统级别事件占百分比,最后的1000代表我们想派发1000个随机事件。
更多Monkey的信息,大家可以参考 http://developer.android.com/tools/help/monkey.html