Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/instrumentation-testing/.bazelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
third_party/android-test/overlay
10 changes: 10 additions & 0 deletions android/instrumentation-testing/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
common --enable_bzlmod
common --java_language_version=17
common --java_runtime_version=remotejdk_17
common --tool_java_language_version=17
common --tool_java_runtime_version=remotejdk_17

test:local_device --test_strategy=exclusive
test:local_device --test_arg=--device_broker_type=LOCAL_ADB_SERVER

try-import %workspace%/.bazelrc.user
1 change: 1 addition & 0 deletions android/instrumentation-testing/.bazelversion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9.1.0
15 changes: 15 additions & 0 deletions android/instrumentation-testing/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module(name = "android_instrumentation_testing")

bazel_dep(name = "android_test_overlay", version = "0.0.0")
bazel_dep(name = "rules_android", version = "0.7.2")

android_sdk_repository_extension = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension")
use_repo(android_sdk_repository_extension, "androidsdk")

android_test = use_extension("@android_test_overlay//:extensions.bzl", "android_test")
use_repo(android_test, "android_test_support")

local_path_override(
module_name = "android_test_overlay",
path = "third_party/android-test",
)
63 changes: 63 additions & 0 deletions android/instrumentation-testing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Android Instrumentation Testing with Bazel

This sample builds a small Java Android app and runs an AndroidX/JUnit4
instrumentation test on a connected local device.

## Run

Point Bazel at an Android SDK:

```shell
export ANDROID_HOME=/path/to/android/sdk
```

Alternatively, keep the SDK path in `.bazelrc.user`:

```text
common --repo_env=ANDROID_HOME=/path/to/android/sdk
```

Build the app and instrumentation APK:

```shell
bazel build //app/src/main:greeter_test_app
```

Run the device test:

```shell
bazel test //app/src/main:greeter_instrumentation_test \
--config=local_device \
--nocache_test_results
```

`--config=local_device` uses the local adb server and runs the test
exclusively. `adb devices` should show one usable device; for multiple devices,
pass an explicit serial with `--test_arg=--device_serial_number=<serial>`.
Use either an emulator or a real device that is already running; this sample
does not create or manage an emulator.

## Approach

The sample uses a small Starlark rule in `bazel/android_instrumentation_test.bzl`
to connect `rules_android` APK providers to AndroidX Test's host-side runner.
The rule adds the target APK, instrumentation APK, and runner to runfiles, and
leaves device selection to the `local_device` Bazel config.

AndroidX Test support is vendored as a separate module in
`third_party/android-test`. The root module reaches it through
`local_path_override` and a module extension that creates
`@android_test_support`. Keeping this as a separate module avoids putting the
AndroidX Test dependency graph and overrides directly in the sample module.

The AndroidX Test repository is generated from an upstream source archive plus
the local `third_party/android-test/overlay` tree. The overlay replaces selected
BUILD files with Bzlmod-compatible labels and carries the small local-device
patches needed for retail devices. It also patches AndroidX Test's host-side
test discovery to use the Android SDK `dexdump` from `@androidsdk//:dexdump`
with modern SDK arguments, instead of falling back to AndroidX Test's embedded
Linux `dexdump_annotations` binary.

`.bazelignore` excludes the overlay from normal package discovery because those
BUILD files are intended for the generated `@android_test_support` repository,
not the root sample workspace.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.instrumentation">

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

<application />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.instrumentation.test">

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

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

<application />
</manifest>
37 changes: 37 additions & 0 deletions android/instrumentation-testing/app/src/main/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
load("@rules_android//android:rules.bzl", "android_binary", "android_library")
load("//bazel:android_instrumentation_test.bzl", "android_instrumentation_test")

android_binary(
name = "app",
manifest = "AndroidManifest.xml",
deps = [":greeter_lib"],
)

android_library(
name = "greeter_lib",
srcs = ["java/com/example/android/instrumentation/Greeter.java"],
custom_package = "com.example.android.instrumentation",
manifest = "AndroidManifest.xml",
visibility = ["//visibility:public"],
)

android_binary(
name = "greeter_test_app",
testonly = True,
srcs = ["java/com/example/android/instrumentation/GreeterInstrumentationTest.java"],
instruments = ":app",
manifest = "AndroidTestManifest.xml",
deps = [
":greeter_lib",
"@android_test_support//:junit",
"@android_test_support//runner/android_junit_runner",
],
)

android_instrumentation_test(
name = "greeter_instrumentation_test",
bootstrap_instrumentation_package = "com.example.android.instrumentation.test",
tags = ["manual"],
test_app = ":greeter_test_app",
test_packages = ["com.example.android.instrumentation.test"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.android.instrumentation;

public final class Greeter {
public String greet(String name) {
String normalizedName = name == null ? "" : name.trim();
if (normalizedName.isEmpty()) {
return "Hello, Android!";
}
return "Hello, " + normalizedName + "!";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.android.instrumentation;

import static org.junit.Assert.assertEquals;

import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public final class GreeterInstrumentationTest {
@Test
public void testDefaultGreeting() {
assertEquals("Hello, Android!", new Greeter().greet(null));
}

@Test
public void testNamedGreeting() {
assertEquals("Hello, Bazel!", new Greeter().greet(" Bazel "));
}
}
1 change: 1 addition & 0 deletions android/instrumentation-testing/bazel/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports_files(["android_instrumentation_test.sh.tpl"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
load("@rules_android//providers:providers.bzl", "AndroidInstrumentationInfo", "ApkInfo")

def _apk_from_target(target, attr_name):
if ApkInfo in target:
apk = target[ApkInfo].signed_apk
if not apk:
fail("%s must provide a signed APK" % attr_name)
return apk

apks = [f for f in target[DefaultInfo].files.to_list() if f.basename.endswith(".apk")]
if len(apks) != 1:
fail("%s must provide exactly one .apk file, got %d" % (attr_name, len(apks)))
return apks[0]

def _runfiles_path(file):
path = file.short_path
if path.startswith("../"):
return path[3:]
return path

def _apparent_label(label):
return "//%s:%s" % (label.package, label.name)

def _android_instrumentation_test_impl(ctx):
test_app = ctx.attr.test_app
test_apk = test_app[ApkInfo].signed_apk
target_apk = test_app[AndroidInstrumentationInfo].target.signed_apk
if not target_apk:
fail("test_app must set instruments to an android_binary that produces a signed APK")

support_apks = [_apk_from_target(apk, "support_apks") for apk in ctx.attr.support_apks]
executable = ctx.actions.declare_file(ctx.label.name)

substitutions = {
"%workspace%": ctx.workspace_name,
"%test_label%": _apparent_label(ctx.label),
"%test_entry_point%": _runfiles_path(ctx.executable._test_entry_point),
"%adb%": _runfiles_path(ctx.file._adb),
"%aapt%": _runfiles_path(ctx.executable._aapt),
"%dexdump%": _runfiles_path(ctx.file._dexdump),
"%target_apk%": _runfiles_path(target_apk),
"%instrumentation_apk%": _runfiles_path(test_apk),
"%support_apks%": " ".join([_runfiles_path(apk) for apk in support_apks]),
"%test_packages%": " ".join([
"additional_test_packages=%s" % package
for package in ctx.attr.test_packages
]),
"%device_broker_type%": ctx.attr.device_broker_type,
"%bootstrap_instrumentation_package%": ctx.attr.bootstrap_instrumentation_package,
"%install_basic_services%": str(ctx.attr.install_basic_services).lower(),
"%install_test_services%": str(ctx.attr.install_test_services).lower(),
"%scan_target_package_for_tests%": str(ctx.attr.scan_target_package_for_tests).lower(),
}

ctx.actions.expand_template(
template = ctx.file._template,
output = executable,
substitutions = substitutions,
is_executable = True,
)

runfiles = ctx.runfiles(files = [
executable,
ctx.executable._test_entry_point,
ctx.file._adb,
ctx.executable._aapt,
ctx.file._dexdump,
target_apk,
test_apk,
] + support_apks)
runfiles = runfiles.merge(ctx.attr._test_entry_point[DefaultInfo].default_runfiles)
runfiles = runfiles.merge(ctx.attr._aapt[DefaultInfo].default_runfiles)

return [DefaultInfo(
executable = executable,
runfiles = runfiles,
)]

android_instrumentation_test = rule(
implementation = _android_instrumentation_test_impl,
attrs = {
"bootstrap_instrumentation_package": attr.string(),
"device_broker_type": attr.string(default = "LOCAL_ADB_SERVER"),
"install_basic_services": attr.bool(default = False),
"install_test_services": attr.bool(default = True),
"scan_target_package_for_tests": attr.bool(default = False),
"support_apks": attr.label_list(allow_files = [".apk"]),
"test_app": attr.label(
mandatory = True,
providers = [[ApkInfo, AndroidInstrumentationInfo]],
),
"test_packages": attr.string_list(),
"_aapt": attr.label(
default = Label("@androidsdk//:aapt_binary"),
executable = True,
cfg = "exec",
),
"_adb": attr.label(
default = Label("@androidsdk//:adb"),
allow_single_file = True,
),
"_dexdump": attr.label(
default = Label("@androidsdk//:dexdump"),
allow_single_file = True,
),
"_template": attr.label(
default = Label("//bazel:android_instrumentation_test.sh.tpl"),
allow_single_file = True,
),
"_test_entry_point": attr.label(
default = Label("@android_test_support//:instrumentation_test_runner"),
executable = True,
cfg = "exec",
),
},
test = True,
)
Loading