前言
想想接触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将会停止并报错。
以上就是我学习了解的一些测试,当然Android的自动化测试还有很多其他的框架,个人能力有限,这里我就只记录了我自己了解的一些方式。详细的使用方式,可以查看下面的文章:
Android自动化测试--Local Unit Tests使用
Android自动化测试--Instrumented Unit Tests使用
Android自动化测试--Espresso使用
Android自动化测试--UI Automator使用
Android自动化测试--Monkey使用
如果你觉得有用,请在Github不吝给我一个Star,非常感谢。
写在最后的话:个人能力有限,欢迎大家在下面吐槽。喜欢的话就为我点一个赞吧。也欢迎 Fork Me On Github 。
作者:fodroid
链接:http://www.jianshu.com/p/cb06c4be07fa
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android自动化测试 UI Automator
最新推荐文章于 2022-10-11 11:13:49 发布