前言
想想接触Android也有三年多的时间了,实际开发也有两年的时间了,好像也很少接触到Android自动化测试,虽然偶有听说,但也没有认真的学习过。相信很多朋友跟我也有一样的经历,对自动化测试不了解,加上项目没有要求,认为自动化测试价值不高,完全是浪费时间。但实际的情况并不是这样,前段时间听一个朋友讲了些Android自动化测试,给了我很深的印象。正因为这样的契机,所以前段时间也花时间学了Android基本的自动化测试。趁最近刚好有空整理了一下自己的学习心得。
大家可以看一下这篇文章,可能会说服你:为什么要进行烦人的单元测试?
Android Testing Support Library
在2015年Google I/O大会上,Google放出了一个Android Testing Support Library,该库提供了大量用于测试 Android 应用的框架。此库提供了一组 API,让您可以为应用快速构建何运行测试代码,包括单元测试 JUnit 4 和功能性用户界面 (UI) 测试。我们可以从Android Studio IDE或命令行运行使用这些 API 创建的测试。 在测试库中包含AndroidJUnitRunner类是一个JUnit测试运行器,可让我们在 Android 设备上运行 JUnit 3 或 JUnit 4 样式测试类,包括使用Espresso和UI Automator测试框架的设备。测试运行器可以将测试软件包和要测试的应用加载到设备、运行测试并报告测试结果。所以后面会讲到的单元测试和UI测试的详细使用,都是基于Android Testing Support Library。
单元测试 JUnit 4
我们在实际项目开发中的时候,都是需要写成千上万个方法或函数,这些函数的功能可能很强大,也可能是很小一个功能,但我们在程序中使用时都是需要经过测试的,保证这一部分功能是正确的。所以说,每编写完一个函数之后,都应该对这个函数的方方面面进行测试,这样的测试我们称之为单元测试。传统的编程方式,进行单元测试是一件很麻烦的事情,我们需要在该程序中调用你需要测试的方法,并且仔细观察运行结果,看看是否有错。正因为如此麻烦,所以就有了很多单元测试框架,JUnit 4就是其中一种。
本地单元测试 Local Unit Tests
这种测试运行在本地开发环境的Java虚拟机上,也不需要连接Android设备或者模拟器,因此并无法获得Android相关的API,所以只能测试只使用Java API的一些功能。
测试类代码编写也很简单,主要通过一些注解来标示,同时可以通过assertXXXX来断言结果
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 4);
}
}
Junit 4注解
- @Before标注setup方法,每个单元测试用例方法调用之前都会调用
- @After标注teardown方法,每个单元测试用例方法调用之后都会调用
- @Test标注的每个方法都是一个测试用例
- @BeforeClass标注的静态方法,在当前测试类所有用例方法执行之前执行
- @AfterClass标注的静态方法,在当前测试类所有用例方法执行之后执行
- @Test(timeout=)为测试用例指定超时时间
断言
Junit提供了一系列断言来判断是pass还是fail
- assertTrue(condition):condition为真pass,否则fail
- assertFalse(condition):condition为假pass,否则fail
- fail():直接fail
- assertEquals(expected, actual):expected equal actual pass,否则fail
- assertSame(expected, actual):expected == actual pass,否则fail
设备单元测试 Instrumented Unit Tests
这种测试方式需要连接Android设备或模拟器。可以利用Android框架API,比如测试需要访问设备信息(如目标应用程序的上下文中)或如果他们需要一个Android 相关的API(如Parcelable或SharedPreferences对象)。在使用上也很简单,相比本地单元测试该测试类必须以 @RunWith(AndroidJUnit4.class) 注解作为前缀。
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("top.qingningshe.test", appContext.getPackageName());
}
}
相比Local Unit Tests 多了访问设备信息、测试筛选
访问设备信息
我们可以使用 InstrumentationRegistry
类访问与测试运行相关的信息。此类包括 Instrumentation对象、目标应用Context对象、测试应用Context对象,以及传递到测试中的命令行参数。
测试筛选
- @RequiresDevice:指定测试仅在物理设备而不在模拟器上运行。
- @SdkSupress:禁止在低于给定级别的 Android API 级别上运行测试。例如,要禁止在低于 18 的所有 API 级别上运行测试,请使用注解 @SDKSupress(minSdkVersion=18)。
- @SmallTest、@MediumTest和@LargeTest:指定测试的运行时长以及运行频率。
UI测试
Espresso
Espresso 测试框架提供了一组 API 来构建 UI 测试,用于测试应用中的用户流。利用这些 API,您可以编写简洁、运行可靠的自动化 UI 测试。Espresso 非常适合编写白盒自动化测试,其中测试代码将利用所测试应用的实现代码详情。
Espresso 测试框架的主要功能包括:
- 灵活的 API,用于目标应用中的视图和适配器匹配。
- 一组丰富的操作 API,用于自动化 UI 交互。
- UI 线程同步,用于提升测试可靠性。
要求 Android 2.2(API 级别 8)或更高版本。
视图匹配
利用Espresso.onView()
方法,您可以访问目标应用中的 UI 组件并与之交互。此方法接受Matcher
参数并搜索视图层次结构,以找到符合给定条件的相应View
实例。您可以通过指定以下条件来优化搜索:
- 视图的类名称 onView(withClassName());
- 视图的内容描述 onView(withContentDescription());
- 视图的ID onView(withId());
- 在视图中显示的文本 onView(withText());
更多的可以查看ViewMatchers。如果搜索成功,onView()
方法将返回一个引用,让您可以执行用户操作并基于目标视图对断言进行测试。
适配器匹配
在AdapterView
布局中,布局在运行时由子视图动态填充。如果目标视图位于某个布局内部,而该布局是从AdapterView
(例如ListView
或GridView
)派生出的子类,则onView()
方法可能无法工作,因为只有布局视图的子集会加载到当前视图层次结构中。因此,需要使用Espresso.onData()
方法访问目标视图元素。Espresso.onData()
方法将返回一个引用,让您可以执行用户操作并根据AdapterView
中的元素对断言进行测试。
//点击spinner
onView(withId(R.id.spinner)).perform(click());
//点击adpaterviewer中类型为String 并且内容为test的文本,
onData(allOf(is(instanceOf(String.class)),is("test"))).perform(click());
操作API
在上面的一段代码中,我们用到了perform(click()),那么除了click()方法还有其他功能强大的方法可以供我们使用,下面列举一些常用的方法:
- click():返回一个点击动作,Espresso利用这个方法执行一次点击操作,就和我们自己手动点击按钮一样。
- clearText():返回一个清除指定view中的文本action,在测试EditText时用的比较多。
- swipeLeft():返回一个从右往左滑动的action,这个在测试ViewPager时特别有用。
- swipeRight():返回一个从左往右滑动的action,这个在测试ViewPager时特别有用。
- swipeDown():返回一个从上往下滑动的action。
- swipeUp():返回一个从下往上滑动的action。
- closeSoftKeyboard():返回一个关闭输入键盘的action。
- pressBack():返回一个点击手机上返回键的action。
- doubleClick():返回一个双击action
- longClick():返回一个长按action
更多的可以查看ViewActions。
校验结果
调用ViewInteraction.check()
和DataInteraction.check()
方法,可以判断UI元素的状态,如果断言失败,会抛出AssertionFailedError异常。
- doesNotExist:断言某一个view不存在。
- matches:断言某个view存在,且符合一列的匹配。
- selectedDescendentsMatch:断言指定的子元素存在,且他们的状态符合一些列的匹配。
onView(withId(R.id.textview)).check(matches(withText("test")));
UI 线程同步
Espresso 的核心是它可以与待测应用无缝同步测试操作的能力。默认情况下,Espresso 会等待当前消息队列中的 UI 事件执行(默认是 AsyncTask)完毕再进行下一个测试操作。这应该能解决大部分应用与测试同步的问题。然而,应用中有一些执行后台操作的对象(比如与网络服务交互)通过非标准方式实现;例如:直接创建和管理线程,以及使用自定义服务。庆幸的是 Espresso 仍然可以同步测试操作与你的自定义资源。
以下是我们需要完成的:
- 实现 IdlingResource 接口并暴露给测试。
- 通过调用ResourceCallback..onTransitionToIdle()通知Espresso。
需要注意的是 IdlingResource 接口是在待测应用中实现的,所以你需要添加依赖:
compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'
下面我们看看官方的例子是如何实现的
public final class CountingIdlingResource implements IdlingResource {
private static final String TAG = "CountingIdlingResource";
private final String resourceName;
private final AtomicInteger counter = new AtomicInteger(0);
private final boolean debugCounting;
// written from main thread, read from any thread.
private volatile ResourceCallback resourceCallback;
// read/written from any thread - used for debugging messages.
private volatile long becameBusyAt = 0;
private volatile long becameIdleAt = 0;
/**
* Creates a CountingIdlingResource without debug tracing.
*
* @param resourceName the resource name this resource should report to Espresso.
*/
public CountingIdlingResource(String resourceName) {
this(resourceName, false);
}
/**
* Creates a CountingIdlingResource.
*
* @param resourceName the resource name this resource should report to Espresso.
* @param debugCounting if true increment & decrement calls will print trace information to logs.
*/
public CountingIdlingResource(String resourceName, boolean debugCounting) {
this.resourceName = checkNotNull(resourceName);
this.debugCounting = debugCounting;
}
@Override
public String getName() {
return resourceName;
}
@Override
public boolean isIdleNow() {
return counter.get() == 0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
/**
* Increments the count of in-flight transactions to the resource being monitored.
*
* This method can be called from any thread.
*/
public void increment() {
int counterVal = counter.getAndIncrement();
if (0 == counterVal) {
becameBusyAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
Log.i(TAG, "Resource: " + resourceName + " in-use-count incremented to: " + (counterVal + 1));
}
}
/**
* Decrements the count of in-flight transactions to the resource being monitored.
*
* If this operation results in the counter falling below 0 - an exception is raised.
*
* @throws IllegalStateException if the counter is below 0.
*/
public void decrement() {
int counterVal = counter.decrementAndGet();
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle now! Tell espresso.
if (null != resourceCallback) {
resourceCallback.onTransitionToIdle();
}
becameIdleAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
if (counterVal == 0) {
Log.i(TAG, "Resource: " + resourceName + " went idle! (Time spent not idle: " +
(becameIdleAt - becameBusyAt) + ")");
} else {
Log.i(TAG, "Resource: " + resourceName + " in-use-count decremented to: " + counterVal);
}
}
checkState(counterVal > -1, "Counter has been corrupted!");
}
/**
* Prints the current state of this resource to the logcat at info level.
*/
public void dumpStateToLogs() {
StringBuilder message = new StringBuilder("Resource: ")
.append(resourceName)
.append(" inflight transaction count: ")
.append(counter.get());
if (0 == becameBusyAt) {
Log.i(TAG, message.append(" and has never been busy!").toString());
} else {
message.append(" and was last busy at: ")
.append(becameBusyAt);
if (0 == becameIdleAt) {
Log.w(TAG, message.append(" AND NEVER WENT IDLE!").toString());
} else {
message.append(" and last went idle at: ")
.append(becameIdleAt);
Log.i(TAG, message.toString());
}
}
}
}
在耗时线程中调用
//耗时操作开始调用
helloWorldServerIdlingResource.increment();
{
//做一些耗时操作
}
//结束后调用
helloWorldServerIdlingResource.decrement();
</br>
UI Automator
UI Automator 测试框架提供了一组 API 来构建 UI 测试,用于在用户应用和系统应用中执行交互。利用 UI Automator API,您可以执行在测试设备中打开“设置”菜单或应用启动器等操作。UI Automator 测试框架非常适合编写黑盒自动化测试,其中的测试代码不依赖于目标应用的内部实现详情。
UI Automator 测试框架的主要功能包括:
- 用于检查布局层次结构的查看器。
- 在目标设备上检索状态信息并执行操作的 API。
- 支持跨应用 UI 测试的 API。
要求 Android 4.3(API 级别 18)或更高版本。
UI Automator 查看器
uiautomatorviewer
工具提供了一个方便的 GUI,可以扫描和分析 Android 设备上当前显示的 UI 组件。您可以使用此工具检查布局层次结构,并查看在设备前台显示的 UI 组件属性。利用此信息,您可以使用 UI Automator(例如,通过创建与特定可见属性匹配的 UI 选择器)创建控制更加精确的测试。
uiautomatorviewer 工具位于 <android-sdk>/tools/ 目录中。
UI Automator API
-
UiDevice
:用于在目标应用运行的设备上访问和执行操作。您可以调用其方法来访问设备属性,如当前屏幕方向或显示尺寸,按“返回”、“主屏幕”或“菜单”按钮等。 -
UiCollection
:枚举容器的 UI 元素以便计算子元素个数,或者通过可见的文本或内容描述属性来指代子元素。 -
UiObject
:表示设备上可见的 UI 元素。 -
UiScrollable
:为在可滚动 UI 容器中搜索项目提供支持。 -
UiSelector
:表示在设备上查询一个或多个目标 UI 元素。 -
Configurator
:允许您设置运行 UI Automator 测试所需的关键参数。
// 初始化 UiDevice
mDevice = UiDevice.getInstance(getInstrumentation());
// 按下home键
mDevice.pressHome();
//在当前主界面,查找一个叫test的元素
UiObject allAppsButton = mDevice.findObject(new UiSelector().description("test"));
// 找到后点击它
allAppsButton.click();
更多详细的使用会在后面实际使用中讲到。
压力测试 Monkey
Monkey是Android中的一个命令行工具,可以运行在模拟器里或实际设备中。它向系统发送伪随机的用户事件流(如按键输入、触摸屏输入、手势输入等),实现对正在开发的应用程序进行压力测试。Monkey测试是一种为了测试软件的稳定性、健壮性的快速有效的方法。
Monkey的特征
- 测试的对象仅为应用程序包,有一定的局限性。
- Monky测试使用的事件流数据流是随机的,不能进行自定义。
- 可对MonkeyTest的对象,事件数量,类型,频率等进行设置。
Monkey使用
adb shell monkey [options] <event-count>
options这个是配置monkey的设置,例如指定启动那个包,不指定将会随机启动所有程序。event-count这个是让monkey发送多少次事件。
adb shell monkey -p com.android.test -v 5000
这就是一个简单的测试,向com.android.test包对应的程序发送5000次随机的事件,-p指定了测试的包名,-v指定了发送的随机事件次数。
Monkey停止条件
- 如果限定了Monkey运行在一个或几个特定的包上,那么它会监测试图转到其它包的操作,并对其进行阻止。
- 如果应用程序崩溃或接收到任何失控异常,Monkey将停止并报错。
- 如果应用程序产生了应用程序不响应(ANR)的错误,Monkey将会停止并报错。