自动化单元测试可以做许多的事情,并帮你节省时间。它也可以被用作快速检验新建工程或进行冒烟测试。Android SDK支持JUnit的自动化单元测试,其自动化测试框架主要是通过Instrumentation来实现的。
什么是Instrumentation?
1. 一般在开发Android程序的时候,需要写一个manifest文件,在其中声明<application>,在启动程序的时候就会先启动这个application,然后在运行过程中根据情况加载相应的Activity,而Activity是需要一个界面的。但是Instrumentation并不是这样的,你可以将Instrumentation理解为一种没有图形界面的,具有启动能力的,用于监控其他类(用Target Package声明)的工具类。这是Android单元测试的主入口,它相当于JUnit当中TestRunner的作用。对于单元测试,我们需要认真了解的就是android.test.InstrumentationTestRunner类。
2.如何在Android中利用Instrumentation来进行测试?
在介绍具体的命令之前,我们先理解一下单元测试的层次: 一组单元测试可以被组织成若干个TestSuite, 每个TestSuite包含若干TestCase,每个TestCase又包含若干个具体的testMethod.
(1). 如果假设com.android.foo是你的测试代码的包的根, 当执行以下命令时,会执行所有的TestCase的所有Test。测试的对象就是在Target Package中指定的包中的代码:
adb shell am instrument -w com.android.foo/android.test.InstrumentationTestRunner
其中-w是指定Instrumentation类的参数标志.
(2).如果你想运行一个TestSuite,首先继承的junit.framework.TestSuite类,实现一个TestSuite(比如叫com.android.foo.MyTestSuite),然后执行以下命令执行此TestSuite
adb shell am instrument -e class com.android.foo.MyTestSuite -w com.android.foo/android.test.InstrumentationTestRunner
其中的-e表示额外的参数,语法为-e [arg1] [value1] [arg2] [value2] …这里用到了class参数。(3). 如果仅仅想运行一个TestCase(比如叫com.android.foo.MyTestCase),则用以下命令:
adb shell am instrument -e class com.android.foo.MyTestCase -w com.android.foo/android.test.InstrumentationTestRunner
(4). 如果仅仅想运行一个Test(比如就是上面MyTestCase的testFoo方法),就这样写:
adb shell am instrument -e class com.android.foo.MyTestCase#testFoo -w com.android.foo/android.test.InstrumentationTestRunner
所有的测试结果会输出到控制台,并会做一系列统计,如标记为E的是Error,标记为F的是Failure,Success的测试则会标记为一个点。这和JUnit的语义一致。如果希望断点调试你的测试,只需要直接在代码上加上断点,然后将运行命令参数的-e后边附加上debug true后运行即可。
3.如何在Android的单元测试中做标记?在android.test.annotation包里定义了几个annotation,包括 @LargeTest,@MediumTest,@SmallTest,@Smoke,和@Suppress。你可以根据自己的需要用这些 annotation来对自己的测试分类。在执行单元测试命令时,可以在-e参数后设置“size large”/ “size medium”/ “size small”来执行具有相应标记的测试。特别的@Supperss可以取消被标记的Test的执行。
下面将详细介绍两种方法:如何创建Android单元测试工程?
1. JUnit测试方法一:在原有工程中配置,生成测试用例
1.1 如上图所示,在原有工程的MainActivity.java 文件上右键单击,选择->New->JUnit Test Case, 并选择父类为android.test.AndroidTestCase (Android测试类要继承AndroidTestCase类),会自动生成MainActivityTest.java,其文件内容如下所示:
package com.test.mytest;
import junit.framework.TestCase;
public class MainActivityTest extends AndroidTestCase {
}
1.2 接下来需要在AndroidManifest.xml中配置JUnit测试环境。主要在Application节点下添加users-library节点,以及instrumentation节点。而且instrumentation的android:targetPackage的值要和AndroidManifest的package属性一样。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.mytest"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.test.mytest" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<uses-library android:name="android.test.runner" />
</application>
</manifest>
1.3 默认情况下一般<instrumentation>为android 自带的android.test.InstrumentationTestRunner。但也可以自己继承重写InstrumentationTestRunner生成自己的TestRunner,并将<instrumentation>指定为自己的TestRunner。例如下面的代码就重写生成了自己的MyJUnitTestRunner,添加功能将测试结果输出到test_result.xml
文件。package com.test.mytest;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
public class MyJUnitTestRunner extends android.test.InstrumentationTestRunner {
private Writer mWriter;
private XmlSerializer mTestSuiteSerializer;
private long mTestStarted;
private static final String JUNIT_XML_FILE = "test_result.xml";
@Override
public void onStart() {
try{
File fileRobo = new File(getTestResultDir(getTargetContext()));
if (!fileRobo.exists()) {
fileRobo.mkdir();
}
if (isSDCardAvaliable()) {
File resultFile = new File(getTestResultDir(getTargetContext()),JUNIT_XML_FILE);
startJUnitOutput(new FileWriter(resultFile));
} else {
startJUnitOutput(new FileWriter(new File(getTargetContext().getFilesDir(), JUNIT_XML_FILE)));
}
} catch (IOException e){
throw new RuntimeException(e);
}
super.onStart();
}
@Override
public void sendStatus(int resultCode, Bundle results) {
super.sendStatus(resultCode, results);
switch (resultCode) {
case REPORT_VALUE_RESULT_ERROR:
case REPORT_VALUE_RESULT_FAILURE:
case REPORT_VALUE_RESULT_OK:
try {
recordTestResult(resultCode, results);
} catch (IOException e) {
throw new RuntimeException(e);
}
break;
case REPORT_VALUE_RESULT_START:
recordTestStart(results);
default:
break;
}
}
@Override
public void finish(int resultCode, Bundle results) {
endTestSuites();
super.finish(resultCode, results);
}
/**
* Create the XML serializer for writing
*/
private XmlSerializer newSerializer(Writer writer) {
try {
XmlPullParserFactory pf = XmlPullParserFactory.newInstance();
XmlSerializer serializer = pf.newSerializer();
serializer.setOutput(writer);
return serializer;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Set the XML file start tag.
*/
private void startJUnitOutput(Writer writer) {
try {
mWriter = writer;
mTestSuiteSerializer = newSerializer(mWriter);
mTestSuiteSerializer.startDocument(null, null);
mTestSuiteSerializer.startTag(null, "testsuites");
mTestSuiteSerializer.startTag(null, "testsuite");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Set the XML file end tag.
*/
private void endTestSuites() {
try {
mTestSuiteSerializer.endTag(null, "testsuites");
mTestSuiteSerializer.endDocument();
mTestSuiteSerializer.flush();
mWriter.flush();
mWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 判断SD卡是否存在
*/
private boolean isSDCardAvaliable(){
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
/**
* 获取测试结果报告文件所在的路径
* @param context 被测工程的context
* @return 返回测试结果报告文件所在的路径
*/
private String getTestResultDir(Context context){
String packageName = "/" + context.getPackageName();
String filepath = context.getCacheDir().getPath() + packageName;
if(android.os.Build.VERSION.SDK_INT < 8){
if (isSDCardAvaliable()) {
filepath = Environment.getExternalStorageDirectory().getAbsolutePath()+ packageName;
}
} else {
if (isSDCardAvaliable()) {
filepath = Environment.getExternalStorageDirectory().getAbsolutePath()+ packageName;
}
}
return filepath;
}
private void recordTestStart(Bundle results) {
mTestStarted = System.currentTimeMillis();
}
private void recordTestResult(int resultCode, Bundle results) throws IOException {
float time = (System.currentTimeMillis() - mTestStarted) / 1000.0f;
String className = results.getString(REPORT_KEY_NAME_CLASS);
String testMethod = results.getString(REPORT_KEY_NAME_TEST);
String stack = results.getString(REPORT_KEY_STACK);
int current = results.getInt(REPORT_KEY_NUM_CURRENT);
int total = results.getInt(REPORT_KEY_NUM_TOTAL);
mTestSuiteSerializer.startTag(null, "testcase");
mTestSuiteSerializer.attribute(null, "classname", className);
mTestSuiteSerializer.attribute(null, "name", testMethod);
if (resultCode != REPORT_VALUE_RESULT_OK) {
mTestSuiteSerializer.startTag(null, "failure");
if (stack != null) {
String reason = stack.substring(0, stack.indexOf('\n'));
String message = "";
int index = reason.indexOf(':');
if (index > -1) {
message = reason.substring(index+1);
reason = reason.substring(0, index);
}
mTestSuiteSerializer.attribute(null, "message", message);
mTestSuiteSerializer.attribute(null, "type", reason);
mTestSuiteSerializer.text(stack);
}
mTestSuiteSerializer.endTag(null, "failure");
} else {
mTestSuiteSerializer.attribute(null, "time", String.format("%.3f", time));
}
mTestSuiteSerializer.endTag(null, "testcase");
if (current == total) {
mTestSuiteSerializer.endTag(null, "testsuite");
mTestSuiteSerializer.flush();
}
}
}<span style="font-size:12px;">
</span>
1.4 添加继承AndroidTestCase的TestCase类,并添加要测试的代码,可以添加任意函数,该函数会被测试框架自动执行。 测试类中的测试方法必须以test开头,例如:package com.test.mytest;
import android.test.AndroidTestCase;
public class MainActivityTest extends AndroidTestCase {
public void testAdd() throws Exception {
TestClass object = new TestClass();
int sum = object.add(1, 2);
assertEquals(3, sum);
}
}
1.5 然后点击右键选中MainActivityTest.java,选择Run As -> Android Junit Test,开始执行case。
1.6 也可以将所有的test case 添加到test suite一次性执行。需要重写TestRunner 的getAllTests()函数,然后右键点击整个android project,选择Run As -> Android Junit Test
package com.test.mytest;
import junit.framework.TestSuite;
public class TestSuiteRunner extends MyJUnitTestRunner {
@Override
public TestSuite getAllTests() {
TestSuite suite = new TestSuite(TestSuiteRunner.class.getCanonicalName());
suite.addTestSuite(com.test.mytest.MainActivityTest.class);
//add other test class here
return suite;
}
}
2. JUnit测试方法二:创建单独的单元测试工程
其实这种方式更加简单,这种方式是单独创建一个单元测试的工程来进行测试。即创建一个 Android Test Project ,然后选择需要单元测试的项目就OK了,通过这种方式进行单元测试的话就不用进行上面的配置,其实创建这种工程的时候,默认已经帮我们配置好了。
2.1 创建一个Android的JUnit项目:
如果你的Eclipse中已经有Android项目,对现有的Android项目,在Eclipse中右键单击,选择“Android Tools”,然后“New Test Project...”。如果是新建Android项目,在"New" -> "Project" -> "Android Test Project",按“Next”按钮...。无论按照上面哪种方式,都会在这个时候创建一个独立的Android测试工程。
2.2 在Android测试工程中创建一个JUnit测试用例:
Android应用程序通常是由一些Activity类组成的。事实上,每一个Activity都可以是一个独立实体,Android SDK中包含了几个类来测试Activity类。现在我们将使用一个。右键单击你的测试项目,选择“New” -> 然后“JUnit Test Case”:在新建JUnit测试用例对话框上填写,使用父类是android.test.ActivityInstrumentTestCase2, 如下:
点击“完成”按钮,这个类就创建成功了。注意:由向导创建的默认构造函数是不正确的。我们需要修改它,让它不带任何参数,调用不同的super()的方法.setUp()方法中,应配置运行这个测试用例所需的所有东西。Activity实例随时可以被getActivity()方法调用。例如:如果我们想在测试中显示一个TextView在Activity上,我们可以实现setUp()方法,如下:
protected void setUp() throws Exception {
super.setUp();
TextView helloText = (TextView) getActivity().findViewById(R.id.hello_textview);
}
2.3 此时你可以创建各种测试,你可以获取到Activity布局上所有控件,以及在应用程序的任何代码。由于使用Activity测试用例,我们可能感兴趣于用户界面,布局,及功能。所有的测试方法必须用“test”做前缀。下面我们创建了一个测试名为“HelloTextVisibility”的方法。如果测试通过或失败,assertFalse()都会被调用。
public void testHelloTextVisibility() {
View container = getActivity().findViewById(R.id.container_layout);
int boundaryWidth = container.getWidth();
int boundaryHeight = container.getHeight();
int[] location = new int[2];
container.getLocationOnScreen(location);
int[] helloTextLocation = new int[2];
helloText.getLocationOnScreen(helloTextLocation);
Rect textRect = new Rect();
helloText.getDrawingRect(textRect);
boolean widerThanBoundary = (textRect.width() > boundaryWidth);
boolean tallerThanBoundary = (textRect.height() > boundaryHeight);
boolean extendsOffRight = location[0] + boundaryWidth > helloTextLocation[0] + textRect.width();
assertTrue("Text wider than boundary", widerThanBoundary);
assertTrue("Text taller than boundary", tallerThanBoundary);
assertTrue("Text goes off right side", extendsOffRight);
}
本文介绍了如何快速添加一个新的测试项目,并在Eclipse的Android项目中使用JUnit对你的app执行自动化测试。单元测试可以为逻辑测试,功能测试和用户界面测试等,不再是专门手动测试移动应用。