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.
mb_live_ API key passed as a Bearer token in the Authorization header.
Prerequisites
- Android SDK / Gradle wrapper (the project’s own
./gradlewis all you need). - Your project has an
androidTestsource set with Espresso tests and atestInstrumentationRunnerconfigured (usuallyandroidx.test.runner.AndroidJUnitRunneror 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:
Build both in one go:
Debug with the variant you test against (e.g. assembleStagingDebug assembleStagingDebugAndroidTest for a stagingDebug variant).
Notes:
- Upload APKs, not AABs. If your CI produces an
.aab, either build the APK variant directly or generate a universal APK withbundletool build-apks --mode=universal. - The app under test should be debuggable (
debuggable true), which every*Debugvariant 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
ClassNotFoundExceptionat instrumentation start.
2. ABI coverage (physical vs. virtual)
There is no separate “simulator build” on Android, but ABI splits can bite you:
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
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 usepackage.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:
-e log true dry-runs the suite and logs test names without executing them.)
6. What to upload
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
Common mistakes checklist
- Test APK built against a different variant/flavor than the uploaded app APK. →
targetPackagemismatch; build both from the same variant in the same Gradle invocation. - Uploaded an
.aabinstead of an.apk. → Build the APK, or convert withbundletool --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 connectedAndroidTestlooking for the test APK. → That task installs and runs locally; useassemble<Variant>AndroidTestto produce the uploadable APK.
7. Pick target devices
List the Android devices available to your organisation, with their exact names to use in thedevices field of the build request:
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: theapp_id from POST /uploadBuild and the test_suite_id from POST /app-automate/espresso/test-suite.
Endpoint
Optional parameters - test selection
These map directly onto
AndroidJUnitRunner instrumentation arguments and apply to the whole run:
Optional parameters - execution control
Optional parameters - environment & reporting
Full request with GPS and sharding
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.
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 ashards 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:
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.
build_id; it identifies the run when you poll status and download results.
9. Check run status
Poll the build with thebuild_id from the launch response (or skip polling entirely by passing callbackURL at trigger time).
session_id (used in the next section to download artifacts).
queued, running, passed, failed, error (infrastructure/setup problem, e.g. install or instrumentation-start failure), timedout.
Poll until terminal
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 reportjunit 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:
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:

