自插桩测试示例

当启动插桩测试时,系统会重启其目标软件包,并且会注入和启动插桩代码以执行测试。一种例外情况是,这里的目标软件包不能是 Android 应用框架本身,即软件包 android,因为这样做会导致出现一种矛盾情况:需要重启 Android 框架,而正是该框架支持系统功能,包括插桩本身。

这意味着,插桩测试无法将本身注入到 Android 框架(也称为系统服务器)以执行测试。为了测试 Android 框架,测试代码只能调用公共 API Surface,或者通过平台源代码树中可用的 Android 接口定义语言 (AIDL) 公开的 API Surface。对于此类测试,针对任何特定软件包都没有意义。因此,按照惯例会将此类插桩声明为针对其自己的测试应用软件包,如其自己的 AndroidManifest.xml 中的 <manifest> 标记所定义。

根据要求,此类测试应用软件包还可以:

  • 捆绑测试所需的 Activity。
  • 与系统共享用户 ID。
  • 使用平台密钥进行签名。
  • 根据框架源代码而不是公共 SDK 进行编译。

此类插桩测试有时称为自插桩。以下是平台源代码中自插桩测试的一些示例:

本文介绍的示例是编写新的插桩测试,其中目标软件包设置为其自己的测试应用软件包。本指南使用以下测试作为示例:

建议您先浏览代码以获得粗略的印象,然后再继续。

确定源代码所在的位置

通常,您的团队已有既定的放置模式,在既定的位置检入代码,并且在既定的位置添加测试。大多数团队拥有单个 git 代码库,或与其他团队共享一个代码库,但有一个包含组件源代码的专用子目录。

假设组件源代码的根位置是在 <component source root>,大多数组件在该位置下具有 srctests 文件夹,以及一些其他文件(如 Android.mk,或拆分为额外的 .mk 文件)、清单文件 AndroidManifest.xml,以及测试配置文件“AndroidTest.xml”。

由于您要添加全新的测试,因此或许需要在组件 src 旁边创建 tests 目录,并为其填充内容。

在某些情况下,您的团队可能会在 tests 下设置更深的目录结构,因为需要将不同的测试套件打包到单独的 apk 中。在这种情况下,您需要在 tests 下创建一个新的子目录。

不管是什么样的结构,您最终都需要在 tests 目录或新建子目录中添加文件,并且文件应类似于示例 gerrit 更改中的 instrumentation 目录中的文件。下面几部分将进一步详细说明各个文件。

清单文件

就像常规应用一样,每个插桩测试模块都需要一个清单文件。如果您将该文件命名为 AndroidManifest.xml 并在 Android.mk 旁边为测试 tmodule 提供该文件,则 BUILD_PACKAGE 核心 makefile 将自动包含该文件。

在继续深入阅读以下内容之前,强烈建议您先查阅应用清单概览

此文档概述了清单文件的基本组成部分及其功能。有关示例,请参阅 platform_testing/tests/example/instrumentation/AndroidManifest.xml

为方便起见,下面附上快照:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android.test.example.helloworld"

    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />

    <application>
        <uses-library android:name="android.test.runner" />
    </application>

    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
                     android:targetPackage="android.test.example.helloworld"
                     android:label="Hello World Test"/>

</manifest>

关于清单文件的一些说明:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android.test.example.helloworld"

package 属性是应用软件包名称:它是 Android 应用框架用来标识应用(在此上下文中即您的测试应用)的唯一标识符。系统中的每个用户只能安装一个采用该软件包名称的应用。

此外,此 package 属性与 ComponentName#getPackageName() 返回的属性相同,而且也与用来通过 adb shell 与各种 pm 子命令进行交互的属性相同。

另请注意,虽然该软件包名称通常与 Java 软件包名称的样式相同,但是实际上两者之间没有什么关系。换句话说,您的应用(或测试)软件包可能包含具有任何软件包名称的类,但另一方面,您可以选择保持简洁性,使应用或测试中的顶级 Java 软件包名称与应用软件包名称完全相同。

android:sharedUserId="android.uid.system"

此代码声明在安装时,应该为此 apk 授予与核心平台相同的用户 ID,即运行时身份。请注意,这取决于是否使用与核心平台相同的证书为 apk 签名(请参阅上一部分中的 LOCAL_CERTIFICATE),不过它们是不同的概念:

  • 某些权限或 API 受签名保护,这需要相同的签名证书
  • 某些权限或 API 需要调用者的 system 用户身份,如果调用软件包与核心平台本身不同,则需要该软件包与 system 共享用户 ID
<uses-library android:name="android.test.runner" />

所有插桩测试都必须采用此设置,因为相关的类打包在一个单独的框架 jar 库文件中,因此在应用框架调用测试软件包时,需要额外的类路径条目。

android:targetPackage="android.test.example.helloworld"

您可能已经注意到,上述代码声明的 targetPackage 与此文件的 manifest 标记中声明的 package 属性相同。如测试基础知识中所述,此类插桩测试通常用于测试框架 API,所以除了它们本身之外,拥有特定的目标应用软件包并不是很有意义。

简单配置文件

每个新的测试模块都必须具有配置文件,以使用模块元数据、编译时依赖项和打包指令来指引编译系统。在大多数情况下,基于 Soong 的 Blueprint 文件选项就足够了。如需了解详情,请参阅简单的测试配置

复杂配置文件

对于这些更复杂的用例,您还需要为 Android 的自动化测试框架 Trade Federation 编写测试配置文件。

测试配置可以指定特殊的设备设置选项和默认参数来提供测试类。有关示例,请参阅 /platform_testing/tests/example/instrumentation/AndroidTest.xml

为方便起见,下面附上快照:

<configuration description="Runs sample instrumentation test.">
  <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
  <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
    <option name="test-file-name" value="HelloWorldTests.apk"/>
  </target_preparer>
  <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
  <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
  <option name="test-suite-tag" value="apct"/>
  <option name="test-tag" value="SampleInstrumentationTest"/>

  <test class="com.android.tradefed.testtype.AndroidJUnitTest">
    <option name="package" value="android.test.example.helloworld"/>
    <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
  </test>
</configuration>

关于测试配置文件的一些说明:

<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
  <option name="test-file-name" value="HelloWorldTests.apk"/>
</target_preparer>

上述代码告知 Trade Federation 使用指定的 target_preparer 将 HelloWorldTests.apk 安装到目标设备上。Trade Federation 中有许多目标准备器可供开发者使用,这些目标准备器可用于确保在测试执行之前正确地设置设备。

<test class="com.android.tradefed.testtype.AndroidJUnitTest">
  <option name="package" value="android.test.example.helloworld"/>
  <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
</test>

上述代码指定要用于执行测试的 Trade Federation 测试类,并传入设备上要执行的软件包,以及测试运行器框架(在本例中为 JUnit)。

有关测试模块配置的更多信息,请参阅此处

JUnit4 功能

通过使用 android-support-test 库作为测试运行器,可以采用新的 JUnit4 样式测试类,并且示例 gerrit 更改包含 JUnit4 功能的一些非常基本的用法。有关示例,请参阅 /platform_testing/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java

虽然测试模式通常特定于组件团队,但有一些普遍有用的使用模式。

@RunWith(JUnit4.class)
public class HelloWorldTest {

JUnit4 的一个显著区别是,不再需要从通用测试基类继承测试,而是在普通 Java 类中编写测试并使用注解来指示某些测试设置和约束。在本例中,我们指示此类应作为 JUnit4 测试运行。

    @BeforeClass
    public static void beforeClass() {
    ...
    @AfterClass
    public static void afterClass() {
    ...
    @Before
    public void before() {
    ...
    @After
    public void after() {
    ...
    @Test
    @SmallTest
    public void testHelloWorld() {
    ...

JUnit4 在方法上使用 @Before@After 注解来执行测试前设置和测试后拆解。同样,JUnit4 可以在方法上使用 @BeforeClass@AfterClass 注解,以便在执行测试类中的所有测试之前执行设置,并在执行测试类中的所有测试之后执行拆解。请注意,类作用域的设置和拆解方法必须是静态方法。对于测试方法,与早期版本的 JUnit 不同,它们不再需要使方法名称以 test 开头,而是每种方法都必须带有 @Test 注解。像往常一样,测试方法必须公开、不声明任何返回值、不接受任何参数,并且可能会抛出异常。

重要提示:测试方法本身带有 @Test 注解;请注意,对于要通过 APCT 执行的测试,它们必须带有测试大小注解:在本例中,将 testHelloWorld 方法注解为 @SmallTest。该注解可以在方法作用域或类作用域内应用。

访问 instrumentation

虽然在基本的 Hello World 示例中未涉及,但是有一种相当普遍的情况是,Android 测试需要访问 Instrumentation 实例:这是核心 API 接口,可提供对应用上下文、Activity 生命周期相关测试 API 等内容的访问权限。

因为 JUnit4 测试不再需要通用基类,所以不再需要通过 InstrumentationTestCase#getInstrumentation() 获取 Instrumentation 实例,新的测试运行器会通过 InstrumentationRegistry(用于存储插桩框架创建的上下文和环境设置)管理该实例。

要访问 Instrumentation 类的实例,只需在 InstrumentationRegistry 类上调用静态方法 getInstrumentation()

Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation()

在本地编译和测试

对于最常见的用例,请使用 Atest

对于需要更繁琐自定义设置的更复杂用例,请遵循插桩说明