二、使用安卓 SDK 理解测试

我们现在知道如何在安卓项目中创建测试,以及如何运行这些测试。现在是时候开始深入挖掘,以识别可用于创建更有用测试的构件了。

在第二章中,我们将涉及以下主题:

  • 常见断言
  • 查看断言
  • 其他断言类型
  • 测试用户界面的助手
  • 模拟物体
  • 使用仪器
  • 测试用例类层次结构
  • 使用外部库

我们将分析这些组件,并在适用时展示它们的使用示例。本章中的例子是有意从包含它们的原始安卓项目中分离出来的。这样做是为了让你只专注于所展示的主题,尽管单个项目中的完整示例可以下载,如后面所述。现在,我们对树木感兴趣,而不是森林。

除了给出的例子,我们还将识别可重用的通用模式,这些模式将帮助您为自己的项目创建测试。

示范应用

已经创建了一个非常简单的应用来演示本章中一些测试的使用。应用的源代码可以从 XXXXXXXXXXX 下载。

以下屏幕截图显示此应用正在运行:

The demonstration application

当阅读本章中的测试说明时,您可以随时参考提供的演示应用来查看测试的运行情况。前面简单的应用有一个可点击的链接,文本输入,点击一个按钮和一个定义好的布局 UI,我们可以逐一测试这些。

深度断言

断言是检查可以评估的条件的方法。如果不满足条件,断言方法将引发异常,从而中止测试的执行。

JUnit API 包含类Assert。这是所有TestCase类的基类,这些类拥有几个对编写测试有用的断言方法。这些继承的方法测试各种条件,并被重载以支持不同的参数类型。根据所检查的条件,它们可以组合成以下不同的集合,例如:

  • 资产质量
  • assertTrue
  • assertFalse
  • 断言 Null
  • assertNotNull
  • 资产相同
  • assertNotSame
  • 失败

测试的条件非常明显,很容易通过方法名识别。或许值得关注的是assertEquals()assertSame()。前者,当用于对象时,断言作为参数传递的两个对象同等地调用对象的equals()方法。后者断言两个对象引用同一个对象。如果在某些情况下equals()没有被类实现,那么assertEquals()assertSame()也会做同样的事情。

当这些断言中的一个在测试中失败时,抛出AssertionFailedException,这表明测试已经失败。

有时候,在开发过程中,您可能需要创建一个您在那个精确的时间没有实现的测试。但是,您想标记测试的创建被推迟了(我们在第 1 章测试入门中这样做了,当时我们只添加了测试方法存根)。在这种情况下,您可以使用fail()方法,该方法总是失败,并使用自定义消息来指示条件:

  public void testNotImplementedYet() {
    fail("Not implemented yet");
  }

尽管如此,fail()还有另一个值得一提的常用用法。如果我们需要测试一个方法是否抛出异常,我们可以用 try-catch 块包围代码,如果没有抛出异常,就强制失败。例如:

public void testShouldThrowException() {
    try {
      MyFirstProjectActivity.methodThatShouldThrowException();
      fail("Exception was not thrown");
    } catch ( Exception ex ) {
      // do nothing
    }
  }

JUnit4 有注释@Test(expected=Exception.class),这取代了在测试异常时使用fail()的需要。有了这个注释,测试只有在预期的异常被抛出时才会通过。

自定义消息

值得一提的是知道所有的assert方法都提供了一个重载的版本,包括一个自定义的String消息。如果断言失败,这个自定义消息将由测试运行人员打印,而不是默认消息。

这背后的前提是,有时,通用错误消息没有揭示足够的细节,测试失败的原因也不明显。一旦您查看了测试报告,这个定制消息对于轻松识别故障非常有用,因此强烈建议您使用这个版本。

以下是使用此建议的简单测试示例:

public void testMax() {
int a = 10;
int b = 20;

int actual = Math.max(a, b);

String failMsg = "Expected: " + b + " but was: " + actual;
assertEquals(failMsg, b, actual);
}

在前面的例子中,我们可以看到另一个帮助您轻松组织和理解测试的实践。这是对保存实际值的变量使用显式名称。

还有其他可用的库有更好的默认错误消息,也有更流畅的测试界面。其中值得一看的就是 Fest(https://code.google.com/p/fest/)。

静态进口

虽然基本的断言方法是从 Assert 基类继承的,但是其他一些断言需要特定的导入。为了提高测试的可读性,有一种模式可以静态地从相应的类中导入断言方法。使用这种模式,而不是:

  public void testAlignment() {
 int margin = 0;
   ...
 android.test.ViewAsserts.assertRightAligned(errorMsg, editText, margin);
  }

我们可以通过添加静态导入来简化它:

import static android.test.ViewAsserts.assertRightAligned;

public void testAlignment() {
   int margin = 0;
 assertRightAligned(errorMsg, editText, margin);
}

查看断言

前面介绍的断言将各种类型作为参数进行处理,但它们仅用于测试简单的条件或简单的对象。

例如,我们有asertEquals(short expected, short actual)来测试short值,assertEquals(int expected, int actual)来测试整数值,assertEquals(Object expected, Object expected)来测试任何Object实例,等等。

通常在 Android 中测试用户界面的时候,会面临方法比较复杂的问题,主要和 Views 有关。在这方面,安卓在android.test.ViewAsserts中提供了一个有大量断言的类(更多的详细信息,请参见

这些方法也被重载以提供不同的条件。在这些断言中,我们可以找到以下内容:

  • assertBaselineAligned:这表示两个视图在其基线上对齐;也就是说它们的基线在同一个 y 位置。
  • assertBottomAligned:这断言两个视图是底部对齐的;也就是说,它们的底边位于同一 y 位置。
  • assertGroupContains:这个断言指定的组包含一个特定的子代一次并且只有一次。
  • assertGroupIntegrity:这表示指定组的完整性。子计数应为> = 0,每个子应不为空。
  • assertGroupNotContains:声明指定的组不包含特定的子组。
  • assertHasScreenCoordinates:这表示视图在可见屏幕上有一个特定的 x 和 y 位置。
  • assertHorizontalCenterAligned:这表明测试视图相对于参考视图水平居中对齐。
  • assertLeftAligned:这表示两个视图左对齐;也就是说,它们的左边在同一个 x 位置。还可以提供可选的边距。
  • assertOffScreenAbove:这表示指定的视图位于可见的屏幕上方。
  • assertOffScreenBelow:这表示指定的视图在可见的屏幕下方。
  • assertOnScreen:这个断言屏幕上有一个视图。
  • assertRightAligned:这断言两个视图是右对齐的;也就是说,它们的右边在同一个 x 位置。还可以指定可选的边距。
  • assertTopAligned:这断言两个视图是顶部对齐的;也就是说,它们的顶部边缘位于同一 y 位置。还可以指定可选的边距。
  • assertVerticalCenterAligned:这表明测试视图相对于参考视图垂直居中对齐。

以下示例显示了如何使用ViewAssertions测试用户界面布局:

  public void testUserInterfaceLayout() {
    int margin = 0;
    View origin = mActivity.getWindow().getDecorView();
    assertOnScreen(origin, editText);
    assertOnScreen(origin, button);
    assertRightAligned(editText, button, margin);
  }

assertOnScreen方法使用原点开始寻找请求的视图。在这种情况下,我们使用顶层窗户装饰视图。如果出于某种原因,您不需要在层次结构中走那么高的,或者如果这种方法不适合您的测试,您可以在层次结构中使用另一个根视图,例如View.getRootView(),在我们的具体示例中,它将是editText.getRootView()

更多的断言

如果之前审查的断言似乎不足以满足您的测试需求,那么在安卓框架中仍然有另一个类涵盖了其他情况。这个类是MoreAsserts

这些方法也被重载以支持不同的参数类型。在这些断言中,我们可以找到以下内容:

  • assertAssignableFrom:这个断言一个对象是可以分配给一个类的。
  • assertContainsRegex:这表示预期的正则表达式匹配指定的String的任何子串。如果没有,它将失败并显示指定的消息。
  • assertContainsInAnyOrder:声明指定的Iterable包含精确预期的元素,但是以任何顺序。
  • assertContainsInOrder:这表示指定的Iterable包含精确的预期元素,但是顺序相同。
  • assertEmpty:这个断言一个Iterable是空的。
  • assertEquals:这个是给一些不在 JUnit 断言中的Collections的。
  • assertMatchesRegex:声明指定的RegexString完全匹配,如果不匹配,则提供的消息失败。
  • assertNotContainsRegex:这表示指定的 Regex 与指定字符串的任何子字符串都不匹配,如果匹配,则提供的消息失败。
  • assertNotEmpty:这个断言 JUnit 断言中没有覆盖的一些集合不是空的。
  • assertNotMatchesRegex:这表示指定的Regex与指定的字符串不完全匹配,如果匹配,则提供的消息失败。
  • checkEqualsAndHashCodeMethods:这是一个用来同时测试equals()hashCode()结果的工具。这将测试应用于两个对象的equals()即是否与指定结果匹配。

以下测试检查通过点击用户界面按钮调用大写方法期间的错误:

@UiThreadTest
public void testNoErrorInCapitalization() {
String msg = "capitalize this text";
editText.setText(msg);

button.performClick();

String actual = editText.getText().toString();
String notExpectedRegexp = "(?i:ERROR)";
String errorMsg = "Capitalization error for " + actual;
assertNotContainsRegex(errorMsg, notExpectedRegexp, actual);
}

如果你不熟悉正则表达式,那就投入一些时间,访问http://developer . Android . com/reference/Java/util/regex/package-summary . html吧,因为会很值!

在这种特殊情况下,我们正在查找结果中包含的不区分大小写的单词ERROR(为此设置标志i)。也就是说,如果由于某种原因,大写在我们的应用中不起作用,并且它包含错误消息,我们可以用断言检测这个条件。

注意,因为这是修改用户界面的测试,所以必须用@UiThreadTest标注;否则,它将无法从不同的线程更改用户界面,我们将收到以下异常:

INFO/TestRunner(610): ----- begin exception -----
INFO/TestRunner(610): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
INFO/TestRunner(610):     at android.view.ViewRoot.checkThread(ViewRoot.java:2932)
[...]
INFO/TestRunner(610):     at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1447)
INFO/TestRunner(610): ----- end exception -----

Touchutils 类

有时,在测试用户界面时,模拟不同种类的触摸事件是有帮助的。这些触摸事件可以用许多不同的方式生成,但可能android.test.TouchUtils是最简单的使用方式。这个类提供了可重用的方法来在从InstrumentationTestCase派生的测试用例中生成触摸事件。

特色方法允许与测试中的用户界面进行模拟交互。TouchUtils类提供了使用正确的 UI 或主线程注入事件的基础设施,因此不需要特殊处理,也不需要使用@UIThreadTest对测试进行注释。

TouchUtils 支持以下功能:

  • 单击视图并释放它
  • 轻按视图(触摸它并快速释放)
  • 长按视图
  • 拖动屏幕
  • 拖动视图

以下测试代表了TouchUtils的典型用法:

    public void testListScrolling() {
        listView.scrollTo(0, 0);

        TouchUtils.dragQuarterScreenUp(this, activity); 
        int actualItemPosition = listView.getFirstVisiblePosition();

        assertTrue("Wrong position", actualItemPosition > 0);
    }

该测试执行以下操作:

  • 将列表重新定位在开始位置,从已知条件开始
  • 滚动列表
  • 检查第一个可见位置,查看它是否正确滚动

即使是最复杂的用户界面也可以通过这种方式进行测试,它将帮助您检测可能影响用户体验的各种情况。

模拟物体

我们已经在第一章中看到了安卓测试框架提供的模拟对象,并评估了不使用真实对象将我们的测试与周围环境隔离的顾虑。

下一章讨论测试驱动开发,如果我们是测试驱动开发的纯粹主义者,我们可以讨论模拟对象的使用,并且更倾向于使用真实对象。马丁·福勒在他的伟大文章中称这两种风格为经典模仿者测试驱动开发二分法模仿者不是存根,可以在http://www.martinfowler.com/articles/mocksArentStubs.html在线阅读。

独立于这个讨论,我们引入模拟对象作为可用的构建块之一,因为有时,在我们的测试中使用模拟对象是推荐的、期望的、有用的,甚至是不可避免的。

Android SDK 在子包android.test.mock中提供了以下类来帮助我们:

  • MockApplication:这是对Application类的模拟实现。所有方法都不起作用并抛出UnsupportedOperationException
  • MockContentProvider:这个是ContentProvider的模拟实现。所有方法都不起作用并抛出UnsupportedOperationException
  • MockContentResolver:这是ContentResolver类的模拟实现,将测试代码与真实的内容系统隔离开来。所有方法都不起作用,抛出UnsupportedOperationException
  • MockContext:这个是一个模拟上下文类,这个可以用来注入其他依赖。所有方法都不起作用,抛出UnsupportedOperationException
  • MockCursor:这个是一个模拟的 Cursor 类,它将测试代码与真实的 Cursor 实现隔离开来。所有方法都不起作用,抛出UnsupportedOperationException
  • MockDialogInterface:这个是DialogInterface类的模拟实现。所有方法都不起作用并抛出UnsupportedOperationException
  • MockPackageManager:这个是PackageManager类的模拟实现。所有方法都不起作用并抛出UnsupportedOperationException
  • MockResources:这个是模拟Resources班。

所有这些类都有使用时抛出UnsupportedOperationException的非函数方法。如果您需要使用这些方法中的一些,或者如果您检测到您的测试在这个Exception上失败,您应该扩展这些基类之一并提供所需的功能。

模拟上下文概述

这个模拟可以被用来将其他依赖项、模拟或者监视器注入到被测试的类中。扩展这个类以提供您想要的行为,覆盖相应的方法。安卓 SDK 提供了一些预构建的模拟对象,每个对象都有一个单独的用例。

孤立上下文类

在您的测试中,您可能会发现需要将测试下的活动与其他安卓组件隔离开来,以防止不必要的交互。这可以是完全隔离,但有时,这种隔离避免了与其他组件的交互,并且为了让您的 Activity 仍然正确运行,需要与系统建立一些连接。

对于那些的情况,安卓 SDK 提供了android.test.IsolatedContext,一个模拟的Context,它不仅阻止了与大部分底层系统的交互,还满足了与其他包或组件交互的需求,比如ServicesContentProviders

文件和数据库操作的替代路径

在某些情况下,我们所需要的只是能够提供到文件和数据库操作的替代路径。例如,如果我们在真实的设备上测试应用,我们可能不想影响现有的数据库,而是使用我们自己的测试数据。

这种情况可以利用不属于android.test.mock子包但属于android.test的另一个类,即RenamingDelegatingContext

这个类允许我们通过在构造函数中指定前缀来改变对文件和数据库的操作。所有其他操作都委托给委托上下文,您也必须在构造函数中指定委托上下文。

假设我们的Activity在测试中使用了一个我们想要控制的数据库,可能引入了专门的内容或夹具数据来驱动我们的测试,而我们不想使用真实的文件。在这种情况下,我们创建一个指定前缀的RenamingDelegatingContext类,我们未更改的 Activity 将使用这个前缀创建任何文件。

例如,如果我们的活动试图访问一个名为birthdays.txt的文件,并且我们提供了一个指定前缀testRenamingDelegatingContext类,那么这个相同的活动将在测试时访问文件testbirthdays.txt

mock contentresolver 类

MockContentResolver类以非函数方式实现所有方法,如果您试图使用它们,将抛出异常UnsupportedOperationException。这个类的原因是将测试从真实内容中分离出来。

假设您的应用使用一个ContentProvider类来输入您的活动信息。您可以使用ProviderTestCase2为此ContentProvider创建单元测试,我们将很快对此进行分析,但是当我们试图针对ContentProvider为活动生成功能或集成测试时,使用什么测试用例并不那么明显。最明显的选择是ActivityInstrumentationTestCase2,主要是如果你的功能测试模拟用户体验,因为你可能需要sendKeys()方法或类似的方法,这些方法在这些测试中很容易得到。

然后你可能会遇到的第一个问题是,不清楚在你的测试中从哪里注入MockContentResolver来使用你的ContentProvider测试数据。也没有办法注入一个MockContext

这个问题将在第 3 章测试秘籍中解决,这里提供了进一步的细节。

测试用例基类

这是 JUnit 框架中所有其他测试用例的基类。它实现了我们在前面的例子中分析的基本方法(setUp())。TestCase类也实现了junit.framework.Test接口,这意味着它可以作为 JUnit 测试运行。

你的安卓测试用例应该总是扩展TestCase或者它的一个后代。

默认构造函数

所有的测试用例都需要一个默认的构造函数,因为有时,根据所使用的测试运行器,这是唯一被调用的构造函数,也用于序列化。

根据文献记载,这种方法并不打算让“凡人”在不调用setName(String name)的情况下使用。

因此,为了安抚众神,一个常见的模式是在这个构造函数中使用一个默认的测试用例名称,然后调用given name构造函数:

public class MyTestCase extends TestCase {
 public MyTestCase() {
 this("MyTestCase Default Name");
 }

   public MyTestCase(String name) {
      super(name);
   }
}

类型

下载示例代码

您可以从您在http://www.packtpub.com的账户下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

给定名称的构造函数

这个构造函数以一个名称作为参数来标记测试用例。它会出现在测试报告中,当你试图识别失败测试的来源时,它会有很大的帮助。

setName()方法

有一些扩展的类不提供给定名称的构造函数。在这种情况下,唯一的选择就是呼叫setName(String name)

AndroidTestCase 基类

这个类可以作为通用安卓测试用例的基类。

当您需要访问文件系统中的安卓资源、数据库或文件时,请使用它。上下文在这个类中存储为一个字段,方便地命名为mContext,如果需要可以在测试中使用,或者也可以使用getContext()方法。

基于此类的测试可以使用Context.startActivity()启动多个活动。

Android SDK 中有各种扩展这个基类的测试用例:

  • ApplicationTestCase<T extends Application>
  • ProviderTestCase2<T extends ContentProvider>
  • ServiceTestCase<T extends Service>

使用AndroidTestCase Java 类时,继承了一些可以使用的基础断言方法;让我们更详细地看看这些。

assertactivityrequires mission()方法

该方法的签名如下:

public void assertActivityRequiresPermission(String packageName, String className, String permission)

描述

此断言方法检查特定活动的启动是否受到特定权限的保护。它采用以下三个参数:

  • packageName:这个是一个字符串,表示要启动的活动的包名
  • className:这个是一个字符串,表示要启动的活动的类别
  • permission:这个是有权限检查的字符串

活动被启动,然后SecurityException被期望,它提到在错误消息中缺少所需的权限。这个断言不处理活动的实际实例化,因此不需要一个工具。

示例

该测试检查MyContactsActivity活动中写入外部存储器所需的android.Manifest.permission.WRITE_EXTERNAL_STORAGE权限的要求:

public void testActivityPermission() {
  String pkg = "com.blundell.tut";
  String activity =  PKG + ".MyContactsActivity";
  String permission = android.Manifest.permission.CALL_PHONE;
  assertActivityRequiresPermission(pkg, activity, permission);
}

类型

始终使用描述来自android.Manifest.permission的权限的常量,而不是字符串,因此如果实现发生变化,您的代码仍然有效。

资产读取内容要求授权方法

该方法的签名如下:

public void assertReadingContentUriRequiresPermission(Uri uri, String permission)

描述

此断言方法检查从特定 URI 读取是否需要作为参数提供的权限。

它采用以下两个参数:

  • uri:这个是需要权限才能查询的 Uri
  • permission:这个是包含查询权限的字符串

如果生成了包含指定权限的SecurityException类,则验证该断言。

示例

该测试尝试读取触点,并验证是否产生了正确的SecurityException:

  public void testReadingContacts() {
    Uri URI = ContactsContract.AUTHORITY_URI;
    String PERMISSION = android.Manifest.permission.READ_CONTACTS;
    assertReadingContentUriRequiresPermission(URI, PERMISSION);
  }

assertwriting contenturirequired session()方法

该方法的签名如下:

public void assertWritingContentUriRequiresPermission (Uri uri, String permission)

描述

这个断言方法检查插入特定的Uri是否需要作为参数提供的权限。

它采用以下两个参数:

  • uri:这个是需要权限才能查询的 Uri
  • permission:这是一个包含查询权限的字符串

如果生成了包含指定权限的SecurityException类,则验证该断言。

示例

该测试尝试写入联系人,并验证是否生成了正确的SecurityException:

  public void testWritingContacts() {
  Uri uri = ContactsContract.AUTHORITY_URI;
   String permission = android.Manifest.permission.WRITE_CONTACTS;
  assertWritingContentUriRequiresPermission(uri, permission);
}

仪器仪表

仪器在任何应用代码运行之前由系统实例化,从而允许监控系统和应用之间的所有交互。

与许多其他安卓应用组件一样,工具实现在标签<instrumentation>下的AndroidManifest.xml中描述。然而,随着 gradel 的出现,这已经为我们自动化了,我们可以在应用的build.gradle文件中更改仪器的属性。您的测试的AndroidManifest文件将自动生成:

defaultConfig {
  testApplicationId 'com.blundell.tut.tests'
testInstrumentationRunner  "android.test.InstrumentationTestRunner"
}

如果不声明,前面代码中提到的值也是默认值,这意味着您不必拥有这些参数就可以开始编写测试。

testApplicationId属性为您的测试定义了包的名称。默认情况下,它是测试包名称+ tests下的应用。您可以使用testInstrumentationRunner声明一个自定义测试运行程序。如果您希望以自定义方式运行测试,例如并行测试执行,这将非常方便。

还有很多其他的参数在开发中,我建议你关注一下谷歌 Gradle 插件网站(http://tools . Android . com/tech-docs/new-build-system/user-guide)。

内部类的 ActivityMonitor

正如前面提到的,仪器类用于监控系统和应用或被测活动之间的交互。内部类工具ActivityMonitor允许监控应用中的单个活动。

示例

让我们假设我们的活动中有一个文本视图,它保存一个网址并设置了自动链接属性:

  <TextView 
       android:id="@+id/link
       android:layout_width="match_parent"
    android:layout_height="wrap_content"
       android:text="@string/home"
    android:autoLink="web" " />

如果我们想验证单击时超链接是否被正确跟随,以及是否调用了某个浏览器,我们可以创建如下测试:

  public void testFollowLink() {
        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_VIEW);
        intentFilter.addDataScheme("http");
        intentFilter.addCategory(Intent.CATEGORY_BROWSABLE);

        Instrumentation inst = getInstrumentation();
        ActivityMonitor monitor = inst.addMonitor(intentFilter, null, false);
        TouchUtils.clickView(this, linkTextView);
        monitor.waitForActivityWithTimeout(3000);
        int monitorHits = monitor.getHits();
        inst.removeMonitor(monitor);

        assertEquals(1, monitorHits);
    } 

在这里,我们将执行以下操作:

  1. 为打开浏览器的意图创建一个意图过滤器。
  2. 基于IntentFilter类为我们的仪器添加一个监视器。
  3. 点击超链接。
  4. 等待活动(希望是浏览器)。
  5. 验证监视器点击次数是否增加。
  6. 取下显示器。

使用监视器,我们甚至可以测试与系统和其他活动的最复杂的交互。这是创建集成测试的非常强大的工具。

仪器化测试用例类

InstrumentationTestCase类是可以访问仪器的各种测试用例的直接或间接基类。这是最重要的直接和间接子类的列表:

  • ActivityTestCase
  • ProviderTestCase2<T extends ContentProvider>
  • SingleLaunchActivityTestCase<T extends Activity>
  • SyncBaseInstrumentation
  • ActivityInstrumentationTestCase2<T extends Activity>
  • ActivityUnitTestCase<T extends Activity>

InstrumentationTestCase级在android.test包里,延伸junit.framework.TestCase,延伸junit.framework.Assert

启动活动和启动活动内容方法

这些实用方法用于从测试中启动活动。如果未使用第二个选项指定意图,则使用默认的意图:

public final T launchActivity (String pkg, Class<T> activityCls, Bundle extras)

模板类参数TactivityCls中用作返回类型,将其使用限制在该类型的活动中。

如果需要指定自定义意图,可以使用下面的代码,该代码还添加了intent参数:

public final T launchActivityWithIntent (String pkg, Class<T> activityCls, Intent intent)

发送键和发送重复键方法

在测试 Activities 的 UI 时,您将会面临模拟与基于 qwerty 的键盘或 DPAD 按钮交互的需求,以发送按键来完成字段、选择快捷方式或在不同的组件中导航。

这就是不同的sendKeyssendRepeatedKeys的用途。

有一个版本的sendKeys接受整键值。它们可以从KeyEvent类中定义的常数中获得。

例如,我们可以这样使用sendKeys方法:

    public void testSendKeyInts() {
        requestMessageInputFocus();
        sendKeys(
                KeyEvent.KEYCODE_H,
                KeyEvent.KEYCODE_E,
                KeyEvent.KEYCODE_E,
                KeyEvent.KEYCODE_E,
                KeyEvent.KEYCODE_Y,
                KeyEvent.KEYCODE_DPAD_DOWN,
                KeyEvent.KEYCODE_ENTER);
        String actual = messageInput.getText().toString();

        assertEquals("HEEEY", actual);
    }

在这里,我们将向被测活动发送HEY字母键,然后使用它们的整数表示发送ENTER键。

或者,我们可以通过连接我们想要发送的键来创建一个字符串,丢弃KEYCODE前缀,并用空格分隔它们,最终被忽略:

      public void testSendKeyString() {
        requestMessageInputFocus();

        sendKeys("H 3*E Y DPAD_DOWN ENTER");
        String actual = messageInput.getText().toString();

        assertEquals("HEEEY", actual);
    }

这里,我们做了与之前测试完全相同的操作,但是我们使用了String "H 3* EY DPAD_DOWN ENTER"。请注意,String中的每个键都可以以重复因子为前缀,后跟*和要重复的键。我们在前面的例子中使用了3*E,和E E E一样,也就是三倍的字母E

如果发送重复的密钥是我们在测试中所需要的,那么还有另一种方法正好适用于这些情况:

public void testSendRepeatedKeys() {
        requestMessageInputFocus();

        sendRepeatedKeys(
                1, KeyEvent.KEYCODE_H,
                3, KeyEvent.KEYCODE_E,
                1, KeyEvent.KEYCODE_Y,
                1, KeyEvent.KEYCODE_DPAD_DOWN,
                1, KeyEvent.KEYCODE_ENTER);
        String actual = messageInput.getText().toString();

        assertEquals("HEEEY", actual);
    }

这是以不同方式实现的相同测试。重复号在每个键的前面。

运行时无读取帮助器方法

runTestOnUiThread方法是一个助手方法,用于在用户界面线程上运行部分测试。我们在方法requestMessageInputFocus()中使用了这个;这样我们就可以在等待应用空闲之前,使用Instrumentation.waitForIdleSync()设置编辑文本的焦点。还有,runTestOnUiThread方法抛出了一个异常,所以我们要处理这个情况:

private void requestMessageInputFocus() {
        try {
            runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    messageInput.requestFocus();
                }
            });
        } catch (Throwable throwable) {
            fail("Could not request focus.");
        }
        instrumentation.waitForIdleSync();
    }

或者,正如我们之前所讨论的,为了在 UI 线程上运行测试,我们可以用@UiThreadTest对其进行注释。然而,有时,我们只需要在 UI 线程上运行部分测试,因为它的其他部分不适合在该线程上运行,例如数据库调用,或者我们正在使用其他助手方法,这些方法本身提供了使用 UI 线程的基础设施,例如TouchUtils方法。

activity testcase 类

这主要是类,它保存了访问 Instrumentation 的其他测试用例的公共代码。

如果您正在为测试用例实现特定的行为,并且现有的替代方法不符合您的需求,那么您可以使用这个类。这意味着您不太可能使用这个类,除非您想要实现一个新的基类供其他测试使用。例如,考虑一个场景,谷歌推出了一个新组件,你想围绕它编写测试(像SuperNewContentProvider)。

如果不是这样,您可能会发现以下选项更适合您的需求:

  • ActivityInstrumentationTestCase2<T extends Activity>
  • ActivityUnitTestCase<T extends Activity>

抽象类android.test.ActivityTestCase扩展了android.test.InstrumentationTestCase并作为其他不同测试用例的基类,如android.test.ActivityInstrumentationTestCaseandroid.test.ActivityInstrumentationTestCase2android.test.ActivityUnitTestCase

android.test.ActivityInstrumentationTestCase测试用例是一个弃用的类,因为 Android API Level 3 (Android 1.5),不应该在更新的项目中使用。虽然很久以前就被弃用了,但它有一个很棒的自动导入名称,所以要小心!

擦洗类方法

scrubClass方法是类中受保护的方法之一:

protected void scrubClass(Class<?> testCaseClass)

它是在几个讨论的测试用例实现中从tearDown()方法调用的,以便清理可能已经被实例化为非静态内部类的类变量,从而避免保存对它们的引用。

这是为了防止大型测试套件的内存泄漏。

如果在访问这些类变量时遇到问题,将引发IllegalAccessException

activitynstrumentationtestcase 2 类

ActivityInstrumentationTestCase2类可能是你最常用来编写功能性安卓测试用例的类。它提供单个活动的功能测试。

该类可以访问仪器,并将使用系统基础设施,通过调用InstrumentationTestCase.launchActivity()来创建测试中的活动。创建后,可以对活动进行操作和监控。

如果您需要提供一个自定义的意图来启动您的活动,在调用getActivity()之前,您可以用setActivityIntent(Intent intent)注入一个意图。

这个测试用例对于通过用户界面测试交互非常有用,因为可以注入事件来模拟用户行为。

施工方

该类只有个公共非弃用构造函数,如下:

ActivityInstrumentationTestCase2(Class<T> activityClass)

对于用作类模板参数的同一活动,应该使用Activity类的实例来调用它。

设置方法

setUp方法是初始化测试用例字段和其他需要初始化的夹具组件的精确位置。

这是一个示例,展示了您可能在测试用例中反复发现的一些模式:

 @Override
 protected void setUp() throws Exception {
   super.setUp();
   // this must be called before getActivity()
   // disabling touch mode allows for sending key events
   setActivityInitialTouchMode(false);

   activity = getActivity();
   instrumentation = getInstrumentation();
   linkTextView = (TextView) activity.findViewById(R.id.main_text_link);
   messageInput = (EditText) activity.findViewById(R.id.main_input_message);
   capitalizeButton = (Button) activity.findViewById(R.id.main_button_capitalize);
 } 

我们执行以下操作:

  1. 调用超级方法。这是一个 JUnit 模式,这里应该遵循它来确保正确的操作。
  2. 禁用触摸模式。要生效,这应该在创建活动之前通过调用getActivity()来完成。它将被测活动的初始触摸模式设置为禁用。触摸模式是安卓用户界面的一个基本概念,在讨论。
  3. 使用getActivity()开始活动。
  4. 去拿仪器。我们可以使用仪器,因为ActivityInstrumentationTestCase2延伸了InstrumentationTestCase
  5. 找到视图并设置字段。在这些操作中,请注意使用的R类来自目标包,而不是测试。

拆卸方法

通常,这种方法会清除在setUp中初始化的内容。例如,如果您正在创建一个集成测试,在测试之前设置一个模拟 web 服务器,那么您可能希望在测试之后将其拆除以释放资源。

在本例中,我们确保我们使用的对象已被处置:

@Override  
protected void tearDown() throws Exception {
    super.tearDown();
      myObject.dispose();
}

providertestcase 2类

这是一个测试用例,旨在测试ContentProvider类。

ProviderTestCase2类还扩展了AndroidTestCase。类模板参数T代表被测的ContentProvider。这个测试的实现使用了IsolatedContextMockContentResolver,它们是我们在本章前面描述的模拟对象。

施工方

这个类只有个公共的非弃用构造函数。具体如下:

ProviderTestCase2(Class<T> providerClass, String providerAuthority)

这应该通过用作类模板参数的同一个ContentProvider类的ContentProvider类的实例来调用。

第二个参数是提供者的权限,通常定义为ContentProvider类中的AUTHORITY常量。

一个例子

这是一个ContentProvider测试的典型例子:

public void testQuery() {
    String segment = "dummySegment";
    Uri uri = Uri.withAppendedPath(MyProvider.CONTENT_URI, segment);
    Cursor c = provider.query(uri, null, null, null, null);
    try {
      int actual = c.getCount();

       assertEquals(2, actual);
    } finally {
        c.close();
  }
}

在这个测试中,我们期望查询返回一个包含两行的游标(这只是一个使用适用于您的特定情况的行数的例子)并断言这个条件。

通常,在setUp方法中,我们使用getProvider()获得对本例中mProvider提供者的引用。

有趣的是,因为这些测试都是使用MockContentResolverIsolatedContext,所以真实数据库的内容并没有受到影响,我们也可以像这样运行破坏性测试:

public void testDeleteByIdDeletesCorrectNumberOfRows() {
    String segment = "dummySegment";
    Uri uri = Uri.withAppendedPath(MyProvider.CONTENT_URI, segment);

    int actual = provider.delete(uri, "_id = ?", new String[]{"1"});

    assertEquals(1, actual);
}

该测试从数据库中删除了一些内容,但之后数据库会恢复到其初始内容,以不影响其他测试。

服务测试案例<测试>

这是一个专门为测试服务而创建的测试案例。行使使用寿命周期的方法,如setupServicestartServicebindServiceshutDownService也包括在此类中。

施工方

这个类只有一个公共的非弃用构造函数。具体如下:

ServiceTestCase(Class<T> serviceClass)

对于用作类模板参数的同一个Service,应该使用Service类的实例来调用它。

测试套件图像。failedtocreatetests class 失败建立测试类别

TestSuiteBuilder.FailedToCreateTests类是一个特殊的TestCase类,用于在build()步骤中指示失败。也就是说,在测试套件创建过程中,如果检测到错误,您将会收到一个像这样的异常,它指示构建测试套件失败:

INFO/TestRunner(1): java.lang.RuntimeException: Exception during suite construction
INFO/TestRunner(1):     at android.test.suitebuilder.TestSuiteBuilder$FailedToCreateTests.testSuiteConstructionFailed(TestSuiteBuilder.java:239)
INFO/TestRunner(1):     at java.lang.reflect.Method.invokeNative(Native Method)
[...]
INFO/TestRunner(1):     at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:520)
INFO/TestRunner(1):     at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1447)

在测试项目中使用库

您的安卓项目可能需要一个外部 Java 库或安卓库。现在,我们将解释如何将这些结合到您准备测试的项目中。请注意,下面解释了作为安卓库的本地模块的用法,但是相同的规则可以应用于外部 JAR (Java 库)文件或外部 AAR(安卓库)文件。

假设在一个活动中,我们从一个属于库的类中创建对象。为了我们的例子,假设库名为dummyLibrary,提到的类为Dummy

所以我们的活动看起来像这样:

import com.blundell.dummylibrary.Dummy;

public class MyFirstProjectActivity extends Activity {
    private Dummy dummy;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final EditText messageInput = (EditText) findViewById(R.id.main_input_message);
        Button capitalizeButton = (Button) findViewById(R.id.main_button_capitalize);
        capitalizeButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                String input = messageInput.getText().toString();
                messageInput.setText(input.toUpperCase());
            }
        });

        dummy = new Dummy();
    }

    public Dummy getDummy() {
        return dummy;
    }

    public static void methodThatShouldThrowException() throws Exception {
        throw new Exception("This is an exception");
    }

}

这个库是一个安卓 AAR 模块,所以它应该以正常方式添加到你的build.gradle依赖项中:

dependencies {
    compile project(':dummylibrary')
}

如果这是一个外部图书馆,你可以用'com.external.lib:name:version'代替project(':dummylibrary')

现在,让我们创建一个简单的测试。从我们以前的经验中,我们知道如果我们需要测试一个活动,我们应该使用ActivityInstrumentationTestCase2,这正是我们将要做的。我们的简单测试如下:

public void testDummy() {
  assertNotNull(activity.getDummy());
}

前面代码中的测试在第一个实例中运行并通过!请注意,在不远的过去(Gradle 之前),测试甚至不会编译。我们将不得不经历重重困难,将测试库添加到我们的安卓测试项目中,或者使 JAR/AAR 文件可以从我们的主项目中导出。这是一个停下来反思 Gradle 和 Android Studio 的强大功能的好时机,它们为我们提供了大量免费的手动设置。

总结

我们研究了最相关的构建块和可重用模式来创建我们的测试。在此过程中,我们:

  • 理解 JUnit 测试中常见的断言
  • 解释了安卓 SDK 中的专门断言
  • 探索安卓模拟对象及其在安卓测试中的使用
  • 举例说明了安卓 SDK 中不同测试用例的使用

现在我们已经有了所有的构件,是时候开始创建越来越多的测试来获得掌握这项技术所需的经验了。

下一章将为您提供在 Android 上何时何地使用不同测试用例的示例。这将使我们在知道当我们有一个特定的场景要测试时应该应用什么样的测试方法方面有很大的专业知识。