Android 插桩测试

7.3 · 7.2 · 7.1 · 7.0 · 6.5

如果您刚开始接触 Bazel,请先参阅使用 Bazel 构建 Android 教程。

并行运行 Android 插桩测试

图 1. 运行并行 Android 插桩测试。

借助 android_instrumentation_test,开发者可以在 Android 模拟器和设备上测试其应用。它利用的是真实的 Android 框架 API 和 Android 测试库。

为了确保封闭性和可重现性,Bazel 会在沙盒中创建并启动 Android 模拟器,从而确保测试始终从干净的状态运行。每个测试都会获得一个隔离的模拟器实例,这样测试就可以并行运行,而无需在测试之间传递状态。

如需详细了解 Android 插桩测试,请参阅 Android 开发者文档

请在 GitHub 问题跟踪器中提交问题。

工作原理

当您首次在 android_instrumentation_test 目标上运行 bazel test 时,Bazel 会执行以下步骤:

  1. 构建测试 APK、被测 APK 及其传递依赖项
  2. 创建、启动并缓存整洁的模拟器状态
  3. 启动模拟器
  4. 安装 APK
  5. 利用 Android Test Orchestrator 运行测试
  6. 关闭模拟器
  7. 报告结果

在后续的测试运行中,Bazel 会从第 2 步中创建的干净缓存状态启动模拟器,因此不会保留之前运行时留下的状态。缓存模拟器状态还有助于加快测试运行速度。

前提条件

确保您的环境满足以下前提条件:

  • Linux。已在 Ubuntu 16.04 和 18.04 上进行了测试。

  • Bazel 0.12.0 或更高版本。通过运行 bazel info release 验证版本。

bazel info release

这会生成类似于以下内容的输出:

release 4.1.0
  • KVM。Bazel 要求模拟器在 Linux 上使用 KVM 进行硬件加速。您可以按照适用于 Ubuntu 的安装说明操作。

如需验证 KVM 是否具有正确的配置,请运行以下命令:

apt-get install cpu-checker && kvm-ok

如果它输出以下消息,则表示您的配置正确无误:

INFO: /dev/kvm exists
KVM acceleration can be used
  • Xvfb。如需运行无头测试(例如在 CI 服务器上),Bazel 需要 X 虚拟帧缓冲区

如需安装该配套应用,请运行以下命令:

apt-get install xvfb

通过运行以下命令,验证 Xvfb 是否已正确安装并安装在 /usr/bin/Xvfb 中:

which Xvfb

输出如下所示:

/usr/bin/Xvfb
  • 32 位库。由于测试基础架构使用的部分二进制文件是 32 位的,因此在 64 位计算机上,请确保能够运行 32 位二进制文件。对于 Ubuntu,请安装以下 32 位库:
sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386

使用入门

以下是 android_instrumentation_test 的典型目标依赖项图:

Android 插桩测试的目标依赖项图

图 2. android_instrumentation_test 的目标依赖关系图。

BUILD 文件

该图会转换为如下所示的 BUILD 文件:

android_instrumentation_test(
    name = "my_test",
    test_app = ":my_test_app",
    target_device = "@android_test_support//tools/android/emulated_devices/generic_phone:android_23_x86",
)

# Test app and library
android_binary(
    name = "my_test_app",
    instruments = ":my_app",
    manifest = "AndroidTestManifest.xml",
    deps = [":my_test_lib"],
    # ...
)

android_library(
    name = "my_test_lib",
    srcs = glob(["javatest/**/*.java"]),
    deps = [
        ":my_app_lib",
        "@maven//:androidx_test_core",
        "@maven//:androidx_test_runner",
        "@maven//:androidx_test_espresso_espresso_core",
    ],
    # ...
)

# Target app and library under test
android_binary(
    name = "my_app",
    manifest = "AndroidManifest.xml",
    deps = [":my_app_lib"],
    # ...
)

android_library(
    name = "my_app_lib",
    srcs = glob(["java/**/*.java"]),
    deps = [
        "@maven//:androidx_appcompat_appcompat",
        "@maven//:androidx_annotation_annotation",
    ]
    # ...
)

规则 android_instrumentation_test 的主要属性如下:

  • test_app:一个 android_binary 目标。此目标包含 Espresso 和 UIAutomator 等测试代码和依赖项。所选的 android_binary 目标必须指定一个指向另一个 android_binary(即被测应用)的 instruments 属性。

  • target_device:一个 android_device 目标。此目标说明了 Bazel 用于创建、启动和运行测试的 Android 模拟器的规范。如需了解详情,请参阅选择 Android 设备的部分

测试应用的 AndroidManifest.xml 必须包含 <instrumentation> 标记。 此标记必须指定目标应用的软件包的属性以及插桩测试运行程序的完全限定类名称 androidx.test.runner.AndroidJUnitRunner

下面是测试应用的 AndroidTestManifest.xml 示例:

<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          package="com.example.android.app.test"
    android:versionCode="1"
    android:versionName="1.0">

    <instrumentation
        android:name="androidx.test.runner.AndroidJUnitRunner"
        android:targetPackage="com.example.android.app" />

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

    <application >
       <!-- ... -->
    </application>
</manifest>

WORKSPACE 依赖项

如需使用此规则,您的项目需要依赖于以下外部代码库:

  • @androidsdk:Android SDK。通过 Android Studio 下载此文件。

  • @android_test_support:托管测试运行程序、模拟器启动器和 android_device 目标。您可以点击此处找到最新版本。

将以下代码行添加到 WORKSPACE 文件中,以启用这些依赖项:

# Android SDK
android_sdk_repository(
    name = "androidsdk",
    path = "/path/to/sdk", # or set ANDROID_HOME
)

# Android Test Support
ATS_COMMIT = "$COMMIT_HASH"
http_archive(
    name = "android_test_support",
    strip_prefix = "android-test-%s" % ATS_COMMIT,
    urls = ["https://github.com/android/android-test/archive/%s.tar.gz" % ATS_COMMIT],
)
load("@android_test_support//:repo.bzl", "android_test_repositories")
android_test_repositories()

Maven 依赖项

如需管理对代码库(例如 Google MavenMaven Central)中 Maven 工件的依赖项,您应使用 Maven 解析器,例如 rules_jvm_external

本页面的其余部分介绍了如何使用 rules_jvm_external 从 Maven 代码库中解析和提取依赖项。

选择 android_device 目标

android_instrumentation_test.target_device 指定要在哪部 Android 设备上运行测试。这些 android_device 目标在 @android_test_support 中定义。

例如,您可以通过运行以下命令来查询特定目标的源代码:

bazel query --output=build @android_test_support//tools/android/emulated_devices/generic_phone:android_23_x86

这会生成类似于以下内容的输出:

# .../external/android_test_support/tools/android/emulated_devices/generic_phone/BUILD:43:1
android_device(
  name = "android_23_x86",
  visibility = ["//visibility:public"],
  tags = ["requires-kvm"],
  generator_name = "generic_phone",
  generator_function = "make_device",
  generator_location = "tools/android/emulated_devices/generic_phone/BUILD:43",
  vertical_resolution = 800,
  horizontal_resolution = 480,
  ram = 2048,
  screen_density = 240,
  cache = 32,
  vm_heap = 256,
  system_image = "@android_test_support//tools/android/emulated_devices/generic_phone:android_23_x86_images",
  default_properties = "@android_test_support//tools/android/emulated_devices/generic_phone:_android_23_x86_props",
)

设备目标名称使用以下模板:

@android_test_support//tools/android/emulated_devices/device_type:system_api_level_x86_qemu2

如需启动 android_device,必须具有所选 API 级别的 system_image。如需下载系统映像,请使用 Android SDK 的 tools/bin/sdkmanager。例如,如需下载 generic_phone:android_23_x86 的系统映像,请运行 $sdk/tools/bin/sdkmanager "system-images;android-23;default;x86"

如需查看 @android_test_support 中受支持的 android_device 目标的完整列表,请运行以下命令:

bazel query 'filter("x86_qemu2$", kind(android_device, @android_test_support//tools/android/emulated_devices/...:*))'

Bazel 目前仅支持基于 x86 的模拟器。为了获得更好的性能,请使用 QEMU2 android_device 目标,而不是 QEMU 目标。

运行测试

如需运行测试,请将以下代码行添加到项目的 project root:/.bazelrc 文件中。

# Configurations for testing with Bazel
# Select a configuration by running
# `bazel test //my:target --config={headless, gui, local_device}`

# Headless instrumentation tests (No GUI)
test:headless --test_arg=--enable_display=false

# Graphical instrumentation tests. Ensure that $DISPLAY is set.
test:gui --test_env=DISPLAY
test:gui --test_arg=--enable_display=true

# Testing with a local emulator or device. Ensure that `adb devices` lists the
# device.
# Run tests serially.
test:local_device --test_strategy=exclusive
# Use the local device broker type, as opposed to WRAPPED_EMULATOR.
test:local_device --test_arg=--device_broker_type=LOCAL_ADB_SERVER
# Uncomment and set $device_id if there is more than one connected device.
# test:local_device --test_arg=--device_serial_number=$device_id

然后,使用其中一种配置运行测试:

  • bazel test //my/test:target --config=gui
  • bazel test //my/test:target --config=headless
  • bazel test //my/test:target --config=local_device

仅使用一个配置,否则测试将失败。

无头测试

借助 Xvfb,您可以使用不带图形界面的模拟器进行测试,这也称为无头测试。如需在运行测试时停用图形界面,请将测试参数 --enable_display=false 传递给 Bazel:

bazel test //my/test:target --test_arg=--enable_display=false

GUI 测试

如果设置了 $DISPLAY 环境变量,则可以在测试运行时启用模拟器的图形界面。为此,请将以下测试参数传递给 Bazel:

bazel test //my/test:target --test_arg=--enable_display=true --test_env=DISPLAY

使用本地模拟器或设备进行测试

Bazel 还支持直接在本地启动的模拟器或已连接的设备上进行测试。传递 --test_strategy=exclusive--test_arg=--device_broker_type=LOCAL_ADB_SERVER 标志以启用本地测试模式。如果有多个已连接的设备,请传递标志 --test_arg=--device_serial_number=$device_id,其中 $device_idadb devices 中列出的设备/模拟器的 ID。

示例项目

如果您正在寻找规范的项目示例,请参阅适用于使用 Espresso 和 UIAutomator 的项目的 Android 测试示例

Espresso 设置

如果您使用 Espresso (androidx.test.espresso) 编写界面测试,则可以使用以下代码段在您的 Bazel 工作区中设置常用 Espresso 工件及其依赖项的列表:

androidx.test.espresso:espresso-core
androidx.test:rules
androidx.test:runner
javax.inject:javax.inject
org.hamcrest:java-hamcrest
junit:junit

整理这些依赖项的一种方法是在 project root/BUILD.bazel 文件中创建 //:test_deps 共享库:

java_library(
    name = "test_deps",
    visibility = ["//visibility:public"],
    exports = [
        "@maven//:androidx_test_espresso_espresso_core",
        "@maven//:androidx_test_rules",
        "@maven//:androidx_test_runner",
        "@maven//:javax_inject_javax_inject"
        "@maven//:org_hamcrest_java_hamcrest",
        "@maven//:junit_junit",
    ],
)

然后,在 project root/WORKSPACE 中添加所需的依赖项:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

RULES_JVM_EXTERNAL_TAG = "2.8"
RULES_JVM_EXTERNAL_SHA = "79c9850690d7614ecdb72d68394f994fef7534b292c4867ce5e7dec0aa7bdfad"

http_archive(
    name = "rules_jvm_external",
    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
    sha256 = RULES_JVM_EXTERNAL_SHA,
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

load("@rules_jvm_external//:defs.bzl", "maven_install")

maven_install(
    artifacts = [
        "junit:junit:4.12",
        "javax.inject:javax.inject:1",
        "org.hamcrest:java-hamcrest:2.0.0.0"
        "androidx.test.espresso:espresso-core:3.1.1",
        "androidx.test:rules:aar:1.1.1",
        "androidx.test:runner:aar:1.1.1",
    ],
    repositories = [
        "https://maven.google.com",
        "https://repo1.maven.org/maven2",
    ],
)

最后,在测试 android_binary 目标中,添加 //:test_deps 依赖项:

android_binary(
    name = "my_test_app",
    instruments = "//path/to:app",
    deps = [
        "//:test_deps",
        # ...
    ],
    # ...
)

提示

读取测试日志

使用 --test_output=errors 可输出失败测试的日志,使用 --test_output=all 可输出所有测试输出。如果您要查找单个测试日志,请前往 $PROJECT_ROOT/bazel-testlogs/path/to/InstrumentationTestTargetName

例如,BasicSample 规范项目的测试日志位于 bazel-testlogs/ui/espresso/BasicSample/BasicSampleInstrumentationTest 中,请运行以下命令:

tree bazel-testlogs/ui/espresso/BasicSample/BasicSampleInstrumentationTest

这会产生以下输出:


$ tree bazel-testlogs/ui/espresso/BasicSample/BasicSampleInstrumentationTest
.
├── adb.409923.log
├── broker_logs
│   ├── aapt_binary.10.ok.txt
│   ├── aapt_binary.11.ok.txt
│   ├── adb.12.ok.txt
│   ├── adb.13.ok.txt
│   ├── adb.14.ok.txt
│   ├── adb.15.fail.txt
│   ├── adb.16.ok.txt
│   ├── adb.17.fail.txt
│   ├── adb.18.ok.txt
│   ├── adb.19.fail.txt
│   ├── adb.20.ok.txt
│   ├── adb.21.ok.txt
│   ├── adb.22.ok.txt
│   ├── adb.23.ok.txt
│   ├── adb.24.fail.txt
│   ├── adb.25.ok.txt
│   ├── adb.26.fail.txt
│   ├── adb.27.ok.txt
│   ├── adb.28.fail.txt
│   ├── adb.29.ok.txt
│   ├── adb.2.ok.txt
│   ├── adb.30.ok.txt
│   ├── adb.3.ok.txt
│   ├── adb.4.ok.txt
│   ├── adb.5.ok.txt
│   ├── adb.6.ok.txt
│   ├── adb.7.ok.txt
│   ├── adb.8.ok.txt
│   ├── adb.9.ok.txt
│   ├── android_23_x86.1.ok.txt
│   └── exec-1
│       ├── adb-2.txt
│       ├── emulator-2.txt
│       └── mksdcard-1.txt
├── device_logcat
│   └── logcat1635880625641751077.txt
├── emulator_itCqtc.log
├── outputs.zip
├── pipe.log.txt
├── telnet_pipe.log.txt
└── tmpuRh4cy
    ├── watchdog.err
    └── watchdog.out

4 directories, 41 files

读取模拟器日志

android_device 目标的模拟器日志存储在名称为 emulator_xxxxx.log/tmp/ 目录中,其中 xxxxx 是随机生成的字符序列。

使用以下命令查找最新的模拟器日志:

ls -1t /tmp/emulator_*.log | head -n 1

针对多个 API 级别进行测试

如果您想针对多个 API 级别进行测试,可以使用列表推理为每个 API 级别创建测试目标。例如:

API_LEVELS = [
    "19",
    "20",
    "21",
    "22",
]

[android_instrumentation_test(
    name = "my_test_%s" % API_LEVEL,
    test_app = ":my_test_app",
    target_device = "@android_test_support//tools/android/emulated_devices/generic_phone:android_%s_x86_qemu2" % API_LEVEL,
) for API_LEVEL in API_LEVELS]

已知问题

  • 分叉的 adb 服务器进程在测试后不会终止
  • 虽然 APK 构建适用于所有平台(Linux、macOS、Windows),但测试仅适用于 Linux。
  • 即使使用 --config=local_adb,用户仍然需要指定 android_instrumentation_test.target_device
  • 如果使用本地设备或模拟器,Bazel 不会在测试后卸载 APK。运行以下命令清理软件包:
adb shell pm list
packages com.example.android.testing | cut -d ':' -f 2 | tr -d '\r' | xargs
-L1 -t adb uninstall