你应该开始编写和启动测试作为你的android程序开发周期的一部分。写的好的测试能够帮助你更早的发现bug和使你对你的代码有信心。
一个测试用例定义了一些对象和方法的集合,用于启动多样的彼此独立的测试。测试用例可以被组织进测试套件内和启动编程,以一个可重复的规则,测试框架提供了测试启动的方式。
这节课将会教你如何使用android的自定义测试框架(基于流行的Junit框架)。你可以编写你的测试用例来验证你程序里的指定行为,和检查在不同的android设备中的一致性表现。你的测试用例也可以作为一种内部代码文档的服务,来描述应用组件的期望行为。
课程
- 配置你的测试环境
- 学习如何创建测试工程.
- 创建和启动一个测试用例
- 学习如何编写测试用例来验证Activity的期望的属性, 和使用 andorid 框架自带的
Instrumentation
测试启动器 来启动它。 - 测试UI组件
- 学习如何测试你的Activity中的特殊UI组件的 行为.
- 创建单元测试
- 学习如何执行单元测试来验证一个隔离的activity的行为。
- 创建功能测试
- 学习如何执行功能测试来验证多个activity之间的交互。
配置你的测试环境
在你开始编写和启动你的测试之前,你应该先配置你的测试环境。这节课教你如何配置Eclipse 来编写和启动测试,和如何通过命令提示行方式使用Gradle框架构建和启动测试。
Note: 为了帮助你开始,这个课程时基于Eclipse和ADT插件的。然而,为了你个人的测试环境,你可以自由选择IDE或者选择命令提示行方式。
配置Eclipse测试环境
Eclipse 和 ADT插件提供了一个可以支持你创建,构建,和启动Android程序测试用例的图形界面的集成开发环境。Eclipse 提供了一个非常使用的特性,它可以为你的Android程序项目自动生成一个合适的新的测试工程。.
配置步骤:
- 下载和安装 ADT 插件,如果你还没装过的话.
- 导入或者创建一个你像要创建的Android工程。
- 生成一个符合你的项目的测试工程 ,你需要为你的项目创建(生成)测试工程:
- 在左侧的 包管理区( Package Explorer), 右键点击你的项目, 选择 Android Tools > New Test Project.
- 在新的创建项目的对话框中(New Android Test Project ), 为你的项目设置对应的属性的值并点击完成.
You should now be able to create, build, and run test cases from your Eclipse environment. 了解更多请阅读Creating and Running a Test Case.
配置命令提示行的方式的测试环境
如果你使用 Gradle version 1.6或者更高版本作为你的构建环境, 你可以使用 Gradle Wrapper配置你的测试环境. 确保在你的 gradle.build
文件,在defaultConfig节点中的 minSdkVersion 属性被设置成 8 or higher. 你可以参考包含在下载文件中的训练课程中的示例 samplegradle.build
文件。
使用 Gradle Wrapper 启动测试:
- 连接一个物理设备到你的机器,或者开启一个模拟器.
- 在你的工程文件夹中,执行下面的命令:
./gradlew build connectedCheck
要学习更多 using Gradle for Android testing, 请参考 Gradle Plugin User Guide.
要学习更多 using command line tools other than Gradle for test development, see Testing from Other IDEs.
创建和启动一个测试用例(Creating and Running a Test Case)
为了验证在你的布局设计和基础行为中没有发生 回退 ,非常重要的是为你的程序的每一个activity添加测试用例。在每一个测试中,你需要去创建每个用例的独特部分, 包括测试装置(test fixture), 预先准备好的测试方法, 和 Activity
的测试方法(函数,method). 你可以启动你的测试来得到一个测试报告. 如果一些测试失败了, 这指示了在你的代码中可能存在缺陷( defect).
注意: 在测试驱动开发(TDD)中,在开发周期中先写出较多甚至全部的程序代码和较晚的启动测试 的方式被取代, 你可以渐进的( progressively) 编写足够的代码去 满足(satisfy)你的测试依赖的需要, 更新你的测试用例去表达(reflect)新的功能性需求, 和多次迭代(iterate)这种方式.
创建一个用例(Create a Test Case)
Activity
测试使用结构化的方式编写。确保放置你的测试在一个独立的包内,区别于下面的测试代码。
通常, 你的测试的包名应该和你的程序的包名类似,而加上".tests"后缀。 在你创建的包内,添加一个java的类。通常,你的测试用例的名字也你要测试的类类似,而以作为“Test”后缀。
要在Eclipse中创建测试用例:
- In the Package Explorer, right-click on the
/src
directory for your test project and select New > Package. - Set the Name field to
<your_app_package_name>.tests
(for example,com.example.android.testingfun.tests
) and click Finish. - Right-click on the test package you created, and select New > Class.
- Set the Name field to
<your_app_activity_name>Test
(for example,MyFirstTestActivityTest
) and click Finish.
配置你的测试装置(Set Up Your Test Fixture)
一个测试装置由很多对象构成,这些对象必须被初始化以用来启动一个或者多个测试。要设置测试装置,你可以重载setUp()
和 tearDown() 方法。测试启动器会自动的 在启动任何其他测试方法之前运行
setUp()
方法,在每个方法执行完毕后执行tearDown()方法。你可以使用这两个方法来初始化和清理操作,以区别于其他的测试方法。
要在Eclipse中设置你的测试装置:
- 在包管理器中( Package Explorer ),在你上次创建的测试用例的类上双击鼠标,将在 java编辑器中(Eclipse Java editor)打开这个文件,这时修改你的类使它集成自
ActivityTestCase
.For example:
public class MyFirstTestActivityTest
extends ActivityInstrumentationTestCase2<MyFirstTestActivity> { - 继续, 添加构造方法和
setUp()
方法, 并且为你要测试的 Activity添加 变量的声明.For example:
public class MyFirstTestActivityTest
extends ActivityInstrumentationTestCase2<MyFirstTestActivity> {
private MyFirstTestActivity mFirstTestActivity;
private TextView mFirstTestText;
public MyFirstTestActivityTest() {
super(MyFirstTestActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mFirstTestActivity = getActivity();
mFirstTestText =
(TextView) mFirstTestActivity
.findViewById(R.id.my_first_test_text_view);
}
}构造方法将被测试启动器调用以用来初始化这个测试类,
setUp()
将在其他测试方法运行之前被调用。
典型情况下, 在 setUp()
中, 你可以:
- 为setUp()调用子类的构造,它在JUnit中是必须的。(感觉不太懂,原文:Invoke the superclass constructor for
setUp()
, which is required by JUnit).
初始化你的测试装置的状态,通过下面的方式:Initialize your test fixture state by:
你可以使用 getActivity()
方法去获得 你要测试的 Activity
的引用。
添加测试前置条件 (Add Test Preconditions)
作为明智的(sanity) 检查, 一个很好的实践就是验证测试装置是否被正确的配置好,和验证你要测试的对象是否被正确的实例化和初始化。你不会希望看到因为测试装置的配置错误而导致测试失败。通常,验证测试装置的方法名称叫做 testPreconditions();
比如, 你可能想添加一个testPreconditons()方法,像下面这样:
public void testPreconditions() {
assertNotNull(“mFirstTestActivity is null”, mFirstTestActivity);
assertNotNull(“mFirstTestText is null”, mFirstTestText);
}
断言( assertion )方法来自于 JUnit Assert
类。一般情况,你可以使用断言去验证一个指定的表达式是否是真(true)。
- 如果条件是 false,断言方法抛出
AssertionFailedError
异常,它是测试启动器典型的报告。你可以提供一个字符串作为断言方法的第一个参数,以使得在发生失败时提供更多详细的上下文信息。 - 如果条件是 true, 测试通过。
在两种情形下,测试启动器继续执行测试用例中的其他测试方法。
添加测试方法验证你的Activity (Add Test Methods to Verify Your Activity)
继续,添加一个或多个测试方法验证Activity的布局和功能行为。
例如, 如果你的 Activity
包含了一个 TextView
, 你可以添加一个测试方法去验证它的文本(Lable) 是否是正确的,像下面这样:
public void testMyFirstTestTextView_labelText() {
final String expected =
mFirstTestActivity.getString(R.string.my_first_test);
final String actual = mFirstTestText.getText().toString();
assertEquals(expected, actual);
}
方法 testMyFirstTestTextView_labelText()
简单的检查了TextView的在布局文件中设置的默认文本是否和期望的在strings.xml中定义的文本一致。
注意: 当你为一个测试方法命名时, 你可以使用 下划线(an underscore) 来区分 被测试的对象 和 被测试的情景。这个风格使得可以更容易的看到被测试的明确的对象。
当执行这样的 字符串类型的比较时,一个较好的实践是从你的资源文件中读取它,而不是 应编码(在代码直接写)。这样可以预防在你更改更改为本后使得你的测试很容易的被中断。
要执行(perform)比较, 需要传入期望的和实际的两个字符串的值作为 assertEquals()的参数,如果值不相同,断言将会抛出一个
AssertionFailedError
异常。
如果你添加了一个 testPreconditions()
方法, 在你的代码中,请放置其他的测试方法在testPreconditions()之后。
要活的一个完成的测试例子, 请阅读 在示例中的 MyFirstTestActivityTest.java
.
构建和启动你的测试(Build and Run Your Test)
你可以很容易的在 EclipseYou 的 包管理中(Package Explorer) 构建和启动你的测试。
要构建和启动你的测试:
- 将你的Android设备和你的机器(电脑)连接,并在设备或者模拟器中,打开 设置 菜单,选择 开发者模式,并确保 USB 调试是打开的。
- 在项目管理器(Project Explorer)中, 右击你刚刚创建的测试类并选择 Run As > Android Junit Test.
- 在 设备选择对话框 中,选择你刚刚的设备,并点击 确定 。
- 在 JUnit 视图(JUnit view)中, 核实测试通过或者失败了。
比如, 如果没有发生错误, 结果类似下面这样:
[图片丢失]
测试视图(UI)组件(Testing UI Components )
典型的, 你的 Activity
包含了 用户接口组件,比如按钮,文本框,复选框,pickers 等。它使得用户可以和你的Android 程序交互。这节课讲述了如何测试一个拥有简单按钮的UI。你可以使用这样的通用步骤来测试更为复杂类型的UI组件。
注意: 在这节课中讲述的UI测试的类型被称为 白盒测试,因为你在这里必须使用应用程序的源代码。安卓测试框架(Android Instrumentation framework )适合用于在应用程序内部创建白盒测试。可供选择的另一个类型是 黑盒测试,这种方式你无法访问应用程序源代码。在测试程序如何和其他程序或者系统进行交互过程,它将会很有用。本节课不讲黑盒测试。要了解更多黑盒测试,请阅读 UI Testing guide.
要获得完整的示例,请阅读 示例代码中的 ClickFunActivityTest.java
使用Instrumentation创建测试用例来进行UI测试
当测试一个包含UI组件的Activity时,在测试下的Activity运行在一个UI线程。然而,测试应用程序本身运行在 与测试应用相同进程的另外的线程中。这意味着你的测试程序可以引用一个视图对象,但是如果它试图去更改那些对象的属性或者向UI线程发送事件,你通常会受到一个 WrongThreadException 错误。
要想安全的注入(inject) Intent
对象到你的Activity中,或者 在UI线程执行测试方法。你可以继承自 ActivityInstrumentationTestCase2 类.
要了解更多,请阅读: Testing on the UI thread .
设置你的测试装置(Set Up Your Test Fixture)
当你为了UI测试而设置测试装置时,你需要在你的setUp() 方法中指定 触摸模式( touch mode )设置 触摸模式为true,以防止 当你在测试方法中以编程方式点击它之后,而 UI控件仅获得焦点(比如,一个按钮将会触发它的 点击 监听器)。确保你在调用getActivity()之前就调用 setActivityInitialTouchMode()
方法。.
例如:
public class ClickFunActivityTest
extends ActivityInstrumentationTestCase2 {
...
@Override
protected void setUp() throws Exception {
super.setUp();
setActivityInitialTouchMode(true);
mClickFunActivity = getActivity();
mClickMeButton = (Button)
mClickFunActivity
.findViewById(R.id.launch_next_activity_button);
mInfoTextView = (TextView)
mClickFunActivity.findViewById(R.id.info_text_view);
}
}
添加测试方法以验证UI行为 (Add Test Methods to Validate UI Behavior)
你的测试目标(goals)可能包括下面这些:
- 当一个Activity启动后,核实 一个按钮被正确的显示出来。
- 核实一个 TextView 在初始是被隐藏的。
- 当一个按钮被按下后,核实一个TextView正确的显示了一个期望的字符串。
下面的章节 演示(demonstrates)了如何实现这些方法
验证一个按钮的布局参数(Verify Button Layout Parameters)
你可以添加一个测试方法去验证按钮被正确的显示,像下面这样:
@MediumTest
public void testClickMeButton_layout() {
final View decorView = mClickFunActivity.getWindow().getDecorView();
ViewAsserts.assertOnScreen(decorView, mClickMeButton);
final ViewGroup.LayoutParams layoutParams =
mClickMeButton.getLayoutParams();
assertNotNull(layoutParams);
assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
}
调用 assertOnScreen()
方法时, 你需要传入 根视图对象 和 你希望在屏幕上展示的视图对象。如果期望的视图没有在根视图内找到,那么会抛出AssertionFailedError
异常,或者测试会通过.
你也可以验证Button对象的布局是否正确,通过它的ViewGroup.LayoutParams对象获得一个引用,再调用断言方法验证Button 对象的宽度和高度属性是否是期望的值。
注解 @MediumTest 指示了这个测试是如何分组的,相对它的绝对运行时。要了解更多 如何使用 测试 尺寸 的注解,请阅读:
Apply Test Annotations.
验证 TextView 的布局参数 ( Verify TextView Layout Parameters)
你可以验证一个TextView在初始化时是隐藏的,像下面这样:
@MediumTest
public void testInfoTextView_layout() {
final View decorView = mClickFunActivity.getWindow().getDecorView();
ViewAsserts.assertOnScreen(decorView, mInfoTextView);
assertTrue(View.GONE == mInfoTextView.getVisibility());
}
你可以调用 getDecorView()
方法来获得一个Activity的装饰视图( decor view )的引用。这个装饰视图 是 在局部继承体系中的最顶级的 ViewGroup (FrameLayout
) 对象。
验证按钮行为 (Verify Button Behavior)
你可以验证当一个按钮按下后,使得一个TextView变为可见,例如:
@MediumTest
public void testClickMeButton_clickButtonAndExpectInfoText() {
String expectedInfoText = mClickFunActivity.getString(R.string.info_text);
TouchUtils.clickView(this, mClickMeButton);
assertTrue(View.VISIBLE == mInfoTextView.getVisibility());
assertEquals(expectedInfoText, mInfoTextView.getText());
}
要编程化的在测试中点击一个按钮,调用 clickView()
.To programmatically click a Button
in your test, call clickView()
. You must pass in a reference to the test case that is being run and a reference to the Button
to (操纵)manipulate.
注意: TouchUtils
这个辅助类 可以很方便的帮助我们模拟 和应用程序的触摸交互。你可以使用这些方法来模拟 点击,滑动,拖放视图控件或者屏幕的行为。
警告: TouchUtils
方法被设计用于从测试线程安全的发送事件到UI线程。你不能在UI线程或者使用了@UIThread
.注解的方法上直接执行 TouchUtils
方法,会引发WrongThreadException 异常。
使用测试注解(Apply Test Annotations)
下面的这些注解可以指示测试方法的尺寸:
@SmallTest
- 标记一个测试运行于小的测试.
@MediumTest
- 标记一个测试运行于中等的测试.
@LargeTest
- 标记一个测试运行于较大的测试.
典型情况, 一个小的测试耗时数毫秒,标记为@SmallTest 。更长一点的(100毫秒或更多)通常标记为@MediumTest或者 @LargeTest
s, 依赖于是否访问本地资源或者访问远程的网络资源。要更多 测试尺寸注解的指导内容,请阅读:Android Tools Protip.
你可以使用其他的测试注解标示你的测试方法,来控制 测试如何组织和运行。扩展阅读: Annotation
类。
创建单元测试 (Creating Unit Tests)
要验证一个Activity的状态或者它与其他 独立的组件(也就是说,与系统的其他部分断开) 的交互,那么一个Activity的单元测试是个很好的选择方式。一个单元测试通常要测试一个最小可能的代码单元(可能是一个方法,类,组件等),而不依赖于系统或者玩过资源,比如,你可以编写一个单元测试来检测一个acitivity有正确的布局或者它正确的触发了一个Intent对象 。
单元测试通常不适用于测试复杂的与系统的UI交互事件。要这么做,你可以使用 ActivityInstrumentationTestCase2 类,请参阅:Testing UI Components.
这节课展示了如果编写一个单元测试区验证“一个Intent被触发去启动其他的Activity”。由于测试运行在一个独立的环境,Intent并没有实际发送到Android胸膛呢,但是你可以检查“Intent对象装载的数据是否是准确的”。
要获得完整的例子,请阅读示例代码中的 LaunchActivityTest.java
.
注意: 相对于系统或者外部的依赖,你可以使用 模拟框架(mocking framework)中的 ” 模拟对象“ ,将模拟对象注入到你的单元测试中。要学习更多 Android提供的模拟框架,请阅读:Mock Object Classes.
创建一个单元测试进行 Activity单元测试
ActivityUnitTestCase
类提供了单个Activity的 隔离测试。要创建它,你的测试类需要集成自 ActivityUnitTestCase
.
一个集成自ActivityUnitTestCase的 Activity
不会自动被 Android 框架(Android Instrumentation)启动. 要独立的启动 Activity
, 你需要显式的调用 startActivity()
方法,并且传入Intent参数 来启动你的目标Acitivity。
For example:
public class LaunchActivityTest
extends ActivityUnitTestCase<LaunchActivity> {
...
@Override
protected void setUp() throws Exception {
super.setUp();
mLaunchIntent = new Intent(getInstrumentation()
.getTargetContext(), LaunchActivity.class);
startActivity(mLaunchIntent, null, null);
final Button launchNextButton =
(Button) getActivity()
.findViewById(R.id.launch_next_activity_button);
}
}
验证其他Acitivity的启动
你的单元测试的目标可能包括:
- 验证 当一个按钮被按下后,一个Intent启动了一个 Acitivity。
- 验证 一个被启动的Activity装载了正确的数据。
要验证一个按钮按下后就触发一个Intent你可以使用 getStartedActivityIntent() 方法
. 通过使用断言方法,你可以验证返回的值不是空,并且包含了所要启动Activity的期望的字符串。如果这两项都为 true ,你就成功了验证了 你的Activity正确的发送了一个Intent.
比如你可以这样实现你的测试方法:
@MediumTest
public void testNextActivityWasLaunchedWithIntent() {
startActivity(mLaunchIntent, null, null);
final Button launchNextButton =
(Button) getActivity()
.findViewById(R.id.launch_next_activity_button);
launchNextButton.performClick();
final Intent launchIntent = getStartedActivityIntent();
assertNotNull("Intent was null", launchIntent);
assertTrue(isFinishCalled());
final String payload =
launchIntent.getStringExtra(NextActivity.EXTRAS_PAYLOAD_KEY);
assertEquals("Payload is empty", LaunchActivity.STRING_PAYLOAD, payload);
}
因为 LaunchActivity
是独立启动的, 你不能使用 TouchUtils
库 去操作UI组件。如果要点击一个按钮,你可以调用 performClick()
方法.
创建功能测试(Creating Functional Tests)
功能测试包含了,验证个体程序组件像用户期待的那样工作。比如,你可以创建一个功能测试来验证“当用户操作界面时,一个Activity正确的启动了目标Activity” .
要为你的Activity创建功能测试
,你的测试类必须继承自 ActivityInstrumentationTestCase2
. 不同于 ActivityUnitTestCase
, 在 ActivityInstrumentationTestCase2
中的测试可以和Android系统进行通讯,并可以发送键盘输入事件和UI视图点击事件 .
要完整的示例,请阅读示例程序中的 SenderActivityTest.java
.
添加测试方法验证功能行为(Add Test Method to Validate Functional Behavior)
你的功能测试的目的可能包含下面这些:
- 当在一个发送者Activity中按下一个UI控件时,验证目标Activity被启动。
- 验证目标Acitivyt显示了 基于用户在发送者Activity中输入的正确的数据。
你可能这么实现你的方法:
@MediumTest
public void testSendMessageToReceiverActivity() {
final Button sendToReceiverButton = (Button)
mSenderActivity.findViewById(R.id.send_message_button);
final EditText senderMessageEditText = (EditText)
mSenderActivity.findViewById(R.id.message_input_edit_text);
// Set up an ActivityMonitor
...
// Send string input value
...
// Validate that ReceiverActivity is started
...
// Validate that ReceiverActivity has the correct data
...
// Remove the ActivityMonitor
...
}
测试等待一个 匹配了监视器的 Activity
,否则在 超时后返回null。如果 接收者Activity被启动,你刚刚设置好的 Activity监视器(ActivityMonitor
)被击中。你可以使用断言方法去验证 接收者Activity确实被启动了,并且 在Activity监视器(ActivityMonitor
)中的被击中的次数总数增到你期望的次数。 .
设置一个Activity监视器(Set up an ActivityMonitor)
要在你的程序汇总监视一个独立的Activity,你可以注册一个Activity监视器。只要一个匹配了你的条件的Activity被启动,该 Activity监视器就会收到一个系统通知。如果匹配达成,监视器的计数总数就会发生变化。
通常,你可以这样使用 Activity监视器:
- 通过
getInstrumentation()
方法获得一个 你的测试场景的 测试仪器(Instrumentation
)的示例 . - 通过
Instrumentation 的一个
addMonitor()方法 添加 一个 测试监视器(
Instrumentation.ActivityMonitor
)的实例。可以通过一个 Inent过滤器(IntentFilter)或者 类名字符串 指定匹配条件。
- 等待指定的
Activity
被启动. - 验证 测试监视器的击中次数增长了。
- 移除监视器.
For example:
// Set up an ActivityMonitor
ActivityMonitor receiverActivityMonitor =
getInstrumentation().addMonitor(ReceiverActivity.class.getName(),
null, false);
// Validate that ReceiverActivity is started
TouchUtils.clickView(this, sendToReceiverButton);
ReceiverActivity receiverActivity = (ReceiverActivity)
receiverActivityMonitor.waitForActivityWithTimeout(TIMEOUT_IN_MS);
assertNotNull("ReceiverActivity is null", receiverActivity);
assertEquals("Monitor for ReceiverActivity has not been called",
1, receiverActivityMonitor.getHits());
assertEquals("Activity is of wrong type",
ReceiverActivity.class, receiverActivity.getClass());
// Remove the ActivityMonitor
getInstrumentation().removeMonitor(receiverActivityMonitor);
使用测试仪器发送键盘录入(Send Keyboard Input Using Instrumentation)
如果你的 Activity
拥有一个 文本输入框( EditText)字段
,你可能想测试用户能否录入内容到这个文本框内。
通常,要在 ActivityInstrumentationTestCase2中发送 字符串输入值 到 一个文本框中,你可以
:
- 使用
runOnMainSync()
方法来启动requestFocus()方法获得焦点,
以 在消息循环中的同步调用方式。 这种方式下,UI现场被阻塞,直到 获得焦点为止。 - 调用
waitForIdleSync()
方法等待UI主线程变成 空闲状态。(这样就是,没有更多的事件需要处理). - 通过调用
sendStringSync() 方法和传入你的输入字符串作为参数,
发送一个文本字符串到EditText
For example:
// Send string input value
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
senderMessageEditText.requestFocus();
}
});
getInstrumentation().waitForIdleSync();
getInstrumentation().sendStringSync("Hello Android!");
getInstrumentation().waitForIdleSync();