> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobileboost.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Espresso

> Package your compiled Espresso test suite and execute it on the MobileBoost device grid

This guide covers how to build and package your compiled Espresso instrumentation suite so it runs correctly on the MobileBoost device grid, and how to trigger and monitor runs via the API. Unlike iOS/XCUITest, Android does not split builds into a physical vs. virtual "slice" - the same pair of APKs runs on both real devices and emulators, as long as the APKs contain the right ABIs (see [ABI coverage](#2-abi-coverage-physical-vs-virtual)).

<Note>
  You do not need to solve signing for our devices. Our grid re-signs both the app under test and the test APK with a common certificate before installing, so instrumentation always runs with matching signatures.
</Note>

All API endpoints below authenticate with your `mb_live_` API key passed as a Bearer token in the `Authorization` header.

## Prerequisites

* Android SDK / Gradle wrapper (the project's own `./gradlew` is all you need).
* Your project has an `androidTest` source set with Espresso tests and a `testInstrumentationRunner` configured (usually `androidx.test.runner.AndroidJUnitRunner` or a subclass).
* You know three identifiers - you'll need them at upload time:
  * **Application ID** of the app under test (must match the APK you upload)
  * **Test application ID** (usually `<applicationId>.test`)
  * **Instrumentation runner class** (from `defaultConfig.testInstrumentationRunner`)

## 1. Build the two APKs

Espresso runs need exactly two artifacts, built from the same variant:

| Artifact       | Gradle task                    | Output                                                                      |
| -------------- | ------------------------------ | --------------------------------------------------------------------------- |
| App under test | `assemble<Variant>`            | `app/build/outputs/apk/<variant>/app-<variant>.apk`                         |
| Test APK       | `assemble<Variant>AndroidTest` | `app/build/outputs/apk/androidTest/<variant>/app-<variant>-androidTest.apk` |

Build both in one go:

```bash theme={null}
./gradlew assembleDebug assembleDebugAndroidTest
```

Replace `Debug` with the variant you test against (e.g. `assembleStagingDebug assembleStagingDebugAndroidTest` for a `stagingDebug` variant).

<Warning>
  Both APKs must come from the same variant of the same build - the test APK's `targetPackage` is baked in at compile time and must match the application ID of the app APK you upload.
</Warning>

Notes:

* Upload **APKs, not AABs**. If your CI produces an `.aab`, either build the APK variant directly or generate a universal APK with `bundletool build-apks --mode=universal`.
* The app under test should be **debuggable** (`debuggable true`), which every `*Debug` variant is by default. Instrumenting a non-debuggable build relies on signature matching, which our re-signing handles, but a debuggable build also enables better failure artifacts (screenshots, hierarchy dumps).
* If minification is enabled on the variant, make sure your ProGuard/R8 rules keep the classes your tests reference - a common source of `ClassNotFoundException` at instrumentation start.

## 2. ABI coverage (physical vs. virtual)

There is no separate "simulator build" on Android, but ABI splits can bite you:

| Target                      | Required ABI                                                                                         |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| Physical devices            | `arm64-v8a`                                                                                          |
| Virtual devices (emulators) | `x86_64` (our emulators also run arm64 translation, but native `x86_64` is faster and more reliable) |

If your build uses `splits.abi` or per-ABI APKs, upload the APK that contains the ABI of the target device - or disable splits for the test build and upload a universal APK. An APK with pure Java/Kotlin code and no native libraries runs everywhere. Uploading an arm64-only APK for an emulator run is the Android equivalent of the iOS "wrong slice" mistake: it installs fine on a physical device but fails on x86\_64 emulators.

## 3. Verify the APKs before uploading

```bash theme={null}
# Application ID of the app under test
aapt dump badging app-debug.apk | grep package

# Test APK: confirm its instrumentation target matches the app's application ID
aapt dump xmltree app-debug-androidTest.apk AndroidManifest.xml | grep -A2 instrumentation

# Confirm which ABIs are included (empty output = no native libs = runs everywhere)
unzip -l app-debug.apk | grep 'lib/'
```

The `android:targetPackage` in the test APK's `<instrumentation>` element must equal the `package` of the app APK. If they differ, the instrumentation will install but refuse to start - the single most common failure.

## 4. Signing - what you do and don't need to do

You do not need to share keystores or match our certificates. We strip the signatures from both APKs and re-sign them with the same certificate before installing, so the app/test signature pair always matches. Building with the default debug keystore is fine.

Because we re-sign, avoid features in the **test APK** that depend on your specific signing certificate (e.g. signature-level permissions against other apps of yours, Google API keys restricted to your release SHA-1). The app under test is re-signed too - if it verifies its own certificate at runtime (tamper detection, Play Integrity, Firebase App Check in enforced mode), disable that check in the variant you upload.

## 5. Optional: get your test list for sharding

If you want to split tests across devices, we can shard automatically, or you can send us the test identifiers. Identifiers use `package.ClassName#testMethod` notation, e.g. `com.mycompany.app.LoginTests#testValidLogin`. Class-level (`com.mycompany.app.LoginTests`) and package-level (`com.mycompany.app.checkout`) identifiers are also accepted in filters.

You can list them locally with:

```bash theme={null}
adb shell am instrument -w -e log true \
  <TEST_APP_ID>/<INSTRUMENTATION_RUNNER>
```

(`-e log true` dry-runs the suite and logs test names without executing them.)

## 6. What to upload

| File                            | Purpose                      | Upload endpoint                          |
| ------------------------------- | ---------------------------- | ---------------------------------------- |
| `app-<variant>.apk`             | App under test               | `POST /uploadBuild`                      |
| `app-<variant>-androidTest.apk` | Compiled Espresso test suite | `POST /app-automate/espresso/test-suite` |

Each upload returns an ID (`app_id` / `test_suite_id`) that you reference when triggering a run. Uploads are reusable across runs - upload once per CI build, trigger as many runs as you like against the same pair.

**Managing uploaded test suites**

```
GET    /app-automate/espresso/test-suites                  # list recent uploads (id, name, upload time)
DELETE /app-automate/espresso/test-suite/<TEST_SUITE_ID>   # remove an upload permanently
```

### Common mistakes checklist

* Test APK built against a different variant/flavor than the uploaded app APK. → `targetPackage` mismatch; build both from the same variant in the same Gradle invocation.
* Uploaded an `.aab` instead of an `.apk`. → Build the APK, or convert with `bundletool --mode=universal`.
* ABI-split arm64 APK uploaded for an emulator run (or x86\_64 for a physical run). → Upload a universal APK or the matching ABI split.
* R8/ProGuard stripped classes the tests reference. → Add keep rules or test against a non-minified variant.
* App verifies its own signing certificate at runtime and detects our re-sign. → Disable tamper/integrity checks in the uploaded variant.
* Ran `./gradlew connectedAndroidTest` looking for the test APK. → That task installs and runs locally; use `assemble<Variant>AndroidTest` to produce the uploadable APK.

## 7. Pick target devices

List the Android devices available to your organisation, with their exact names to use in the `devices` field of the build request:

```
GET /app-automate/espresso/devices
```

```bash theme={null}
curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  "https://api.mobileboost.io/app-automate/espresso/devices"
```

Device identifiers combine model and OS version, e.g. `Google Pixel 8-15.0` or `Samsung Galaxy S23-14.0`. Virtual devices (emulators) are listed alongside physical ones and flagged as such.

## 8. Trigger a test run

Once both files are uploaded, start a run with the IDs returned by the two upload calls: the `app_id` from `POST /uploadBuild` and the `test_suite_id` from `POST /app-automate/espresso/test-suite`.

**Endpoint**

```
POST /app-automate/espresso/build
```

**Minimal request**

```bash theme={null}
curl -X POST "https://api.mobileboost.io/app-automate/espresso/build" \
  -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "<APP_ID>",
    "testSuite": "<TEST_SUITE_ID>",
    "devices": ["Google Pixel 8-15.0"]
  }'
```

**Required parameters**

| Field       | Description                                                               |
| ----------- | ------------------------------------------------------------------------- |
| `app`       | The `app_id` returned by `POST /uploadBuild`.                             |
| `testSuite` | The `test_suite_id` returned by `POST /app-automate/espresso/test-suite`. |
| `devices`   | List of device identifiers from `GET /app-automate/espresso/devices`.     |

**Optional parameters - test selection**

These map directly onto `AndroidJUnitRunner` instrumentation arguments and apply to the whole run:

| Field                            | Description                                                                                        |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `class`                          | Run only these classes/methods. Values are `package.ClassName` or `package.ClassName#testMethod`.  |
| `package`                        | Run only tests in these Java packages.                                                             |
| `annotation` / `skip-annotation` | Run or exclude tests carrying a given annotation (e.g. `com.mycompany.app.SmokeTest`).             |
| `size`                           | Run only tests annotated `@SmallTest` / `@MediumTest` / `@LargeTest` (`small`, `medium`, `large`). |

**Optional parameters - execution control**

| Field                    | Description                                                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `shards`                 | Split the suite across devices and run in parallel (see below).                                                                            |
| `clearPackageData`       | Clear the app's data between test classes (Test Orchestrator `clearPackageData`), isolating state between tests.                           |
| `singleRunnerInvocation` | Run all tests in one instrumentation invocation instead of one per test - much faster for large suites, at the cost of per-test isolation. |
| `instrumentationOptions` | Extra `-e key value` pairs passed verbatim to the instrumentation runner (custom runner arguments).                                        |

**Optional parameters - environment & reporting**

| Field                 | Description                                                                                                                                              |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project`, `buildTag` | Free-text labels for grouping and filtering runs.                                                                                                        |
| `gpsLocation`         | Location applied to every device before tests start.                                                                                                     |
| `locale`, `language`  | Set device locale/language before the run (e.g. `de_DE`).                                                                                                |
| `video`               | Record a video per session (default `true`).                                                                                                             |
| `deviceLogs`          | Capture logcat for the run (default `true`).                                                                                                             |
| `networkLogs`         | Capture the app's network traffic as a HAR file (default `false`).                                                                                       |
| `coverage`            | Collect Jacoco code coverage; download it per session after the run (default `false`). Requires the variant to be built with `testCoverageEnabled true`. |
| `callbackURL`         | We `POST` the final build status to this URL when the run reaches a terminal state - use instead of (or in addition to) polling.                         |

**Full request with GPS and sharding**

```bash theme={null}
curl -X POST "https://api.mobileboost.io/app-automate/espresso/build" \
  -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "<APP_ID>",
    "testSuite": "<TEST_SUITE_ID>",
    "devices": ["Google Pixel 8-15.0"],
    "project": "My Android App",
    "buildTag": "main-2026-07-15",

    "gpsLocation": { "latitude": 40.698979, "longitude": -73.9958661 },

    "shards": {
      "numberOfShards": 2,
      "mapping": [
        {
          "name": "shard-1",
          "strategy": "class",
          "values": ["com.mycompany.app.LoginTests", "com.mycompany.app.CheckoutTests#testAddToCart"]
        },
        {
          "name": "shard-2",
          "strategy": "package",
          "values": ["com.mycompany.app.search"]
        }
      ]
    }
  }'
```

### GPS location

`gpsLocation` overrides the device's location for the whole run; it is applied on each device before your first test starts and cleared automatically at teardown.

```json theme={null}
{ "latitude": 40.698979, "longitude": -73.9958661, "altitude": 0.0 }
```

| Field       | Required | Notes                  |
| ----------- | -------- | ---------------------- |
| `latitude`  | yes      | Decimal degrees.       |
| `longitude` | yes      | Decimal degrees.       |
| `altitude`  | no       | Meters, default `0.0`. |

If you omit `gpsLocation`, physical devices keep their real GPS and virtual devices use our neutral default. The mock location is injected at the platform level, so `FusedLocationProviderClient` and `LocationManager` both observe it - no changes to your app or tests needed.

### Sharding

Sharding is optional. Provide a `shards` object to split the suite across devices and run them in parallel.

**Auto-distribute** - give only `numberOfShards`; we spread the discovered tests evenly across that many shards:

```json theme={null}
{ "numberOfShards": 3 }
```

**Explicit test cases per shard** - provide a `mapping` array. Each entry names a shard and selects its tests with a `strategy` and a `values` list. `numberOfShards` must match the number of `mapping` entries.

| Strategy     | Values                                                             |
| ------------ | ------------------------------------------------------------------ |
| `class`      | `package.ClassName` or `package.ClassName#testMethod` identifiers. |
| `package`    | Java package names; the shard runs every test under them.          |
| `annotation` | Fully-qualified annotation class names.                            |

```json theme={null}
{
  "numberOfShards": 2,
  "mapping": [
    { "name": "shard-1", "strategy": "class", "values": ["com.mycompany.app.LoginTests"] },
    { "name": "shard-2", "strategy": "package", "values": ["com.mycompany.app.search"] }
  ]
}
```

**Response**

```json theme={null}
{
  "message": "Success",
  "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3"
}
```

Store the `build_id`; it identifies the run when you poll status and download results.

## 9. Check run status

Poll the build with the `build_id` from the launch response (or skip polling entirely by passing `callbackURL` at trigger time).

```
GET /app-automate/espresso/build/<BUILD_ID>
```

```bash theme={null}
curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID"
```

The build carries an overall status plus one session per device/shard combination. Each session has its own `session_id` (used in the next section to download artifacts).

```json theme={null}
{
  "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3",
  "status": "running",
  "sessions": [
    {
      "session_id": "9f1c2a7b6e3d4f018a2b5c6d7e8f9012",
      "device": "Google Pixel 8-15.0",
      "shard": "shard-1",
      "status": "passed",
      "tests": { "total": 12, "passed": 12, "failed": 0, "skipped": 0 }
    },
    {
      "session_id": "1a2b3c4d5e6f70819aabbccddeeff0011",
      "device": "Google Pixel 8-15.0",
      "shard": "shard-2",
      "status": "running",
      "tests": { "total": 8, "passed": 3, "failed": 0, "skipped": 0 }
    }
  ]
}
```

Status values (both build and session): `queued`, `running`, `passed`, `failed`, `error` (infrastructure/setup problem, e.g. install or instrumentation-start failure), `timedout`.

**Poll until terminal**

```bash theme={null}
while :; do
  STATUS=$(curl -s -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
    "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID" | jq -r '.status')
  echo "status: $STATUS"
  case "$STATUS" in
    queued|running) sleep 10 ;;
    *) break ;;
  esac
done
```

**Stop a running build**

Abort a build early (e.g. a newer commit superseded it in CI). Sessions already finished keep their results; running sessions are terminated and marked as stopped.

```
POST /app-automate/espresso/build/<BUILD_ID>/stop
```

```bash theme={null}
curl -X POST -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/stop"
```

## 10. Retrieve results and artifacts

Once the build reaches a terminal status, fetch the machine-readable report at the build level and the raw artifacts per session.

**Build-level test report**

```
GET /app-automate/espresso/build/<BUILD_ID>/report?format=<junit|json>
```

```bash theme={null}
curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/report?format=junit" \
  -o results.xml
```

`junit` returns a standard JUnit XML your CI can ingest; `json` returns the same pass/fail data as structured JSON.

**Session details and artifacts**

Each session (one per device/shard, listed in the build status response) exposes its per-test results and download URLs for the raw artifacts:

```
GET /app-automate/espresso/build/<BUILD_ID>/session/<SESSION_ID>
```

```bash theme={null}
curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/session/$SESSION_ID"
```

```json theme={null}
{
  "session_id": "9f1c2a7b6e3d4f018a2b5c6d7e8f9012",
  "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3",
  "device": "Google Pixel 8-15.0",
  "shard": "shard-1",
  "status": "failed",
  "tests": { "total": 12, "passed": 11, "failed": 1, "skipped": 0 },
  "testcases": [
    {
      "class": "com.mycompany.app.LoginTests",
      "name": "testValidLogin",
      "status": "passed",
      "duration_seconds": 14.2
    },
    {
      "class": "com.mycompany.app.LoginTests",
      "name": "testInvalidPassword",
      "status": "failed",
      "duration_seconds": 31.7,
      "failure_message": "androidx.test.espresso.NoMatchingViewException: ..."
    }
  ],
  "artifacts": {
    "video_url": "https://...",
    "device_log_url": "https://...",
    "instrumentation_log_url": "https://...",
    "network_log_url": "https://..."
  }
}
```

| Artifact                  | Contents                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `video_url`               | Screen recording of the session (present unless `video: false`).                                                                |
| `device_log_url`          | Full logcat for the session (present unless `deviceLogs: false`).                                                               |
| `instrumentation_log_url` | Raw `am instrument` output - the first place to look when a session ends in `error` (instrumentation crashed or never started). |
| `network_log_url`         | HAR capture of the app's traffic (only when `networkLogs: true`).                                                               |

Artifact URLs are pre-signed and expire; download promptly or re-fetch the session to get fresh ones.

**Code coverage**

If the build was triggered with `coverage: true`, download the Jacoco execution data per session and merge/report it with your local Jacoco tooling:

```
GET /app-automate/espresso/build/<BUILD_ID>/session/<SESSION_ID>/coverage
```

```bash theme={null}
curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \
  "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/session/$SESSION_ID/coverage" \
  -o shard-1-coverage.ec
```
