Android自动化测试--学习浅谈

前言

想想接触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(例如ListViewGridView)派生出的子类,则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运行在一个或几个特定的包上,那么它会监测试图转到其它包的操作,并对其进行阻止。
  • 如果应用程序崩溃或接收到任何失控异常,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 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,732评论 25 707
  • Instrumentation介绍 Instrumentation是个什么东西? Instrumentation测...
    打不死的小强qz阅读 7,765评论 2 39
  • 标签(空格分隔): Android 单元测试的好处:Martin Fowler在《重构》里面还解释了为什么单元测试...
    背影杀手不太冷阅读 5,812评论 3 25
  • 星期二。雨。 邮政包裹后续-------又找了一次商家客服,还是没等到答案,晚上气势汹汹地找了维权中心,最后还是再...
    紅蜻蜓阅读 159评论 0 0
  • 条条道路通罗马,财务自由之路也有千万条,只要达到“老有所养、病有所医、亲有所护”的基础条件,加上每月收入大于每月支...
    富兰克刘阅读 1,388评论 3 29