自插桩测试示例

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

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

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

  • 捆绑测试所需的 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 的清单文件。如需使用 BUILD_PACKAGE 核心 makefile 自动纳入此文件,请在测试模块的 Android.mk 文件旁提供此文件。

如果您不熟悉 AndroidManifest.xml 文件,请参阅应用清单概览

下面是一个示例 AndroidManifest.xml 文件:

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

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

    <instrumentation android:name="androidx.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 注解。跟以往一样,测试方法必须公开、不声明任何返回值、不接受任何参数,并且可能会抛出异常。

插桩类访问权限

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

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

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

Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation()

在本地构建和测试

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

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