Skip to content

Early Adopter Offer:Get 40% off Core & Scale for your first year with code EARLYQFView pricing

Espresso test reporting

Qualflare ships a native Espresso reporter that runs inside the instrumented app and records what the run actually did — screenshots, steps, per-attempt retry history and your own metadata — then reaches the host without a single adb command. If you would rather add nothing to the build, the JUnit-XML that ./gradlew connectedAndroidTest already writes still uploads as-is. This guide covers both, CI patterns for emulators, and the hosted, historical analysis — AI failure clustering, flaky-test scoring, and per-launch risk — that sits on top.

Espresso’s native JUnit-XML output, explained

Pipeline diagram: Gradle's connectedAndroidTest task produces JUnit-XML automatically, uploaded with qf collect --format espresso, landing in Qualflare for AI clustering, flaky scoring, and risk

Espresso is a Gradle-based, in-process UI testing framework — part of AndroidX Test, scoped to testing within your own app (an espresso-remote extension covers multi-process scenarios inside that app). It complements, rather than competes with, UI Automator, which tests from outside the app’s process and can drive other apps and system UI — Espresso stays in-app.

Running the instrumented test task uses AndroidJUnitRunner, which writes results as a documented, automatic side effect of the task — no reporter to add, no config to edit:

# Run the instrumented suite via Gradle — writes results automatically
./gradlew connectedAndroidTest

# JUnit-XML lands at:
# app/build/outputs/androidTest-results/connected/**/*.xml
# A matching HTML report lands at:
# app/build/reports/androidTests/connected/index.html

Both paths are documented behavior of the Android Gradle plugin’s command-line test tasks — not a side effect you have to opt into. Any CI job that runs connectedAndroidTest already has a JUnit-XML file sitting in build/outputs when the task finishes — with no format to configure and no conversion step, unlike iOS, where Xcode’s .xcresult bundles need a converter before they become JUnit-XML (see our XCTest reporting guide). That file is the floor, not the ceiling: it carries a status, a duration and a class name per test, and nothing else. Everything the run saw — the screenshot at the moment of failure, the steps leading to it, whether a retry turned it green — is gone by the time the XML is written. That is what the native reporter is for.

Why the raw XML (and its HTML report) isn’t enough

The auto-generated HTML report is genuinely useful — open index.html and you get a browsable summary of the run with zero setup. But like Playwright’s HTML reporter, it’s built for inspecting one run. It’s written to a local build/reports folder and overwritten the next time the task runs. There’s no history across CI builds, no aggregation when a suite is split across multiple emulators, and no way to see whether a test has gotten flakier over the last two weeks. You can archive each run’s XML and HTML as CI artifacts, but a pile of zip files per build is storage, not reporting — nothing connects build #1481 to build #1482, and nobody opens them.

For anything beyond a single run, you need a place that collects results over time and analyzes them: trend lines, flakiness scores, failure grouping. That’s the gap the rest of this guide fills — first the CI plumbing, then the analysis layer.

CI patterns: GitHub Actions, GitLab, Jenkins

GitHub Actions. Android instrumented tests need a running emulator, so most GitHub Actions setups use a community action like reactivecircus/android-emulator-runner to boot one, then run connectedAndroidTest inside it. Upload results even when tests fail — a reporting step that only runs on green builds is useless on the day you need it:

# .github/workflows/android.yml
- name: Run Espresso tests on an emulator
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 34
    script: ./gradlew connectedAndroidTest

- name: Upload results to Qualflare
  if: always() # upload even when tests fail — that's the point
  run: qf myapp collect app/build/outputs/androidTest-results/connected/**/*.xml --format espresso

GitLab CI. GitLab renders JUnit XML natively in the pipeline’s Tests tab, the same way it does for any framework — run the task on a runner with an SDK, emulator (or hardware-accelerated KVM), and Gradle already set up, then declare the XML as a JUnit report:

# .gitlab-ci.yml — JUnit XML doubles as GitLab's native test report
espresso:
  script:
    - ./gradlew connectedAndroidTest
  artifacts:
    when: always
    reports:
      junit: app/build/outputs/androidTest-results/connected/**/*.xml
    paths:
      - app/build/reports/androidTests/connected/

Jenkins. Same idea: run ./gradlew connectedAndroidTest, hand the resulting XML to the classic junit 'app/build/outputs/androidTest-results/connected/**/*.xml' pipeline step for Jenkins’ own build-level pass/fail tracking, then upload the same file with qf collect for the analysis layer Jenkins doesn’t provide.

Whatever the platform, the principle is the same as every other framework Qualflare supports: run the suite, upload the results file with if: always() semantics — one extra step, not a rewrite.

Common Espresso reporting problems (and fixes)

Mobile CI flakiness is a measured, growing problem, not just a feeling: Bitrise’s 2025 Mobile Insights Report found the share of teams experiencing test flakiness grew from 10% in 2022 to 26% in 2025. Most of the causes below are specific to running a UI test against a virtual device rather than a plain JVM.

  • Emulator instability in CI. CI-hosted emulators are often headless and, without hardware acceleration (KVM on Linux runners), meaningfully slower than a developer’s local machine — timing-sensitive assertions that pass locally can fail intermittently in CI simply because the device is slower, not because the app is broken.
  • Animations causing intermittent failures. Google’s own Espresso setup guide is direct about this: “To avoid flakiness, we highly recommend that you turn off system animations” — Window animation scale, Transition animation scale, and Animator duration scale, all under Developer options — on any device or emulator used for testing (developer.android.com).
  • Async operations without an IdlingResource. Espresso’s synchronization only covers operations posted to the app’s MessageQueue — network calls, database writes, and background work outside that queue need an IdlingResource registered, or Espresso can act before the async work finishes. Google’s docs warn that the usual workaround — Thread.sleep() — “might still fail sometimes when executed on slower devices,” which describes a CI emulator almost exactly.
  • Sharding across multiple emulators. AndroidJUnitRunner supports splitting a suite with -e numShards / -e shardIndex instrumentation arguments, so teams commonly run N emulators in parallel, each executing one shard. Unlike Playwright, there’s no built-in merge tool — each shard writes its own separate XML file:
# AndroidJUnitRunner shards natively — split into 4, run each on its own emulator/job
adb shell am instrument -w -e numShards 4 -e shardIndex 0 \
  com.myapp.test/androidx.test.runner.AndroidJUnitRunner
# (repeat with shardIndex 1, 2, 3 on parallel CI jobs)
# No merge-reports equivalent for Espresso — upload each shard's XML separately
qf myapp collect shard-0-results.xml --format espresso
qf myapp collect shard-1-results.xml --format espresso
# ...repeat per shard; all land in the same project
  • The results file is missing after a timed-out run. AndroidJUnitRunner writes its XML when the task finishes — if the CI job is killed by a timeout (a common failure mode when a cold-booting emulator eats into the test budget) or runs out of memory, the file may never be written. Set the CI job timeout comfortably above how long the suite plus emulator boot actually takes, so the task ends and reports rather than getting killed mid-run.

Send Espresso results to Qualflare

The native reporter is a test-only dependency plus one instrumentation argument. It runs inside the app under test, so it sees the run rather than its summary:

// app/build.gradle.kts
dependencies {
    androidTestImplementation("com.qualflare:qualflare-espresso:0.1.0")
}

android {
    defaultConfig {
        // JUnit 4 has no ServiceLoader hook, so the listener is named explicitly
        testInstrumentationRunnerArguments["listener"] =
            "com.qualflare.espresso.QualflareRunListener"
    }
}

That argument names the listener as a string because JUnit 4 has no ServiceLoader hook — which is also why it composes with whatever runner you already have, Hilt’s included, where shipping an AndroidJUnitRunner subclass would fight it. The AAR carries its own ProGuard rules, so a minified androidTest APK needs nothing from you.

Then run the suite you already run. The report is written through androidx.test storage into the directory Gradle already collects, so it arrives on the host by itself:

./gradlew connectedAndroidTest

# The report is pulled to the host by Gradle itself — no adb command
qf myapp collect app/build/outputs/connected_android_test_additional_output

That no-adb path holds from API 29, which is where Gradle starts passing additionalTestOutputDir. On API 24–28 it passes nothing, so the reporter writes into the app’s own files directory and prints the exact commands to pull it — including the flag that keeps the APKs installed, because connectedAndroidTest otherwise uninstalls the app and deletes the report with it. Both paths are verified on emulators at API 24, 29 and 34, plain and under Android Test Orchestrator.

The reporter makes no network call — it writes files, and the CLI uploads them. What a test can add to its own report:

@Test
public void signsIn() {
    Qualflare.label("team", "identity");
    Qualflare.tag("smoke");
    Qualflare.step("enter credentials", () -> {
        onView(withId(R.id.email)).perform(typeText("[email protected]"));
    });
}

// Screenshot on failure, while the app is still on screen.
// The ordering is load-bearing: the rule must be INSIDE the activity's rule.
@Rule
public final RuleChain rules = RuleChain
        .outerRule(new ActivityScenarioRule<>(LoginActivity.class))
        .around(new QualflareRule());

Automatic capture cannot live in the listener, and the reason is worth knowing if you have ever tried it: JUnit runs the @After methods and then reports the failure, so by the time a listener hears about it ActivityScenarioRule has already closed the activity — and the screenshot is of the launcher. Only a rule declared inside the activity’s rule sees the app.

Without the reporter: uploading JUnit-XML

If you would rather add nothing to the build, the XML is already there once the task finishes — you get one case per test with a status and a duration, and nothing inside it. Point the CLI at the file:

# Upload the XML — --format labels it Espresso for detection
qf myapp collect app/build/outputs/androidTest-results/connected/**/*.xml --format espresso

The general shape is qf <project> collect [files...] [flags]. Useful flags: --environment, --branch, --commit, and --dry-run to preview what would be uploaded. In CI it’s the one extra step shown in the GitHub Actions snippet above — and identical in GitLab CI, Bitbucket Pipelines, and Jenkins. Authenticate the CLI once with your Qualflare access token, stored as a CI secret — see the CLI docs.

What you get on top of raw XML

  • AI failure clustering. When a backend change breaks 30 Espresso tests across a dozen activities and fragments, Qualflare groups them by root cause so you triage a handful of clusters instead of 30 stack traces.
  • Flaky-test scoring, two ways. The native reporter records every attempt at a test, so a rerun that turned green arrives already marked flaky with its failures intact. On the XML path that metadata does not exist — JUnit-XML has nowhere to put it — so Qualflare scores flakiness by tracking each test’s pass/fail pattern across launches instead, surfacing the intermittent ones even when no single run flags them.
  • Per-launch risk. Every CI run becomes a “launch” with a risk rating, the failing areas, and recommended next steps — a ship / don’t-ship signal that arrives with the results, across every emulator, API level, or shard you test against.
  • History & trends. Pass rate, slowest tests, and flakiness over time across branches and shards — the aggregation the local HTML report can’t do.
  • Context & defects. Failures keep their JUnit stack trace and logcat context where captured, and you can spin up a defect straight from a failing run.

Espresso’s HTML report vs Qualflare

  Espresso HTML report Qualflare
History across CI runs—Yes
Aggregates sharded / multi-emulator runsManual (no built-in merge)Yes
AI failure clustering (root cause)—Yes
Flaky-test scoring over time—Yes
Screenshot at the moment of failure—With the reporter
Steps, labels, tags and links per test—With the reporter
Per-attempt retry history—With the reporter
Local, zero-setup, offlineYes—
Human-readable without extra toolingYesYes

They’re complementary: keep the auto-generated HTML report for local debugging, add Qualflare for hosted, historical CI observability.

Get AI analysis on your Espresso runs

Start free — run connectedAndroidTest, upload with qf collect, and get your first AI analysis in minutes.

Get Started Free

Building out the rest of your mobile stack? See our guides to XCTest reporting for iOS and Maestro reporting for cross-platform E2E flows, or the full list of 23+ supported frameworks. For the bigger picture on getting Android and iOS results into one place, read unifying Android and iOS test reporting, fixing flaky mobile tests, or the complete guide to mobile test management. Want the detail on what the JUnit file does and does not carry, and the Android traps in capturing the rest? See what Espresso’s JUnit XML leaves out, measured on emulators from API 24 to 34. Weighing tools? See how Qualflare compares to other test management platforms, or browse all framework reporting guides.

Frequently asked questions

Does Qualflare run my Espresso tests on real devices or emulators?

No. Qualflare is a results-management and observability layer, not a device-execution cloud — it never provisions or touches emulators, simulators, or real devices, and it doesn’t schedule your test run. It only needs the JUnit-XML file your run already produced, so it works identically whether that run happened on a local emulator, a CI-hosted emulator (GitHub Actions, GitLab), a self-hosted device farm, or a third-party device cloud like Firebase Test Lab, BrowserStack, or Sauce Labs.

Which Android test frameworks does Qualflare support?

Espresso has a native reporter that runs on the device. UI Automator tests run through the same AndroidJUnitRunner and the same JUnit 4 notifications, and the reporter never requires Espresso at runtime — it declares every androidx.test dependency compileOnly and classifies failures by exception type name rather than by type, precisely so a suite with no Espresso on its classpath still reports. Our own device suite exercises the Espresso path, so treat UI Automator as supported by design rather than by test. Maestro has its own native reporter for cross-platform Android/iOS flows. Any other Android runner that emits JUnit-compatible XML works through Qualflare’s generic ingestion even without a named integration. See the full list on the frameworks page.

Do I need the Qualflare reporter, or is JUnit-XML enough?

Either works. Running ./gradlew connectedAndroidTest writes JUnit-XML automatically, and Qualflare uploads it as-is — one case per test with a status, a duration and a class name. The native reporter (com.qualflare:qualflare-espresso, a test-only dependency plus one instrumentation argument) adds what that file has nowhere to put: a screenshot taken at the moment of failure while the app is still on screen, the steps leading to it, per-attempt retry history, and labels, tags and links written by the test itself. Start with the XML if you cannot change the build; add the reporter when you want to know why a test failed rather than that it did.

Does Qualflare have Espresso-specific support?

Yes — a native reporter that runs inside the instrumented app, on the device. It knows Espresso’s own exception family, so a NoMatchingViewException is reported as a failed assertion rather than an infrastructure error (which is what a generic parser makes of the commonest Espresso failure), an AppNotIdleException becomes a timeout, and a @BeforeClass that throws still produces a case instead of quietly shrinking the suite. If you upload JUnit-XML instead, that path is the generic JUnit-XML-compatible one shared with every other XML framework, and --format espresso only labels the launch.

How do I send Espresso results to Qualflare?

With the native reporter: add androidTestImplementation("com.qualflare:qualflare-espresso:0.1.0") and the listener instrumentation argument, run ./gradlew connectedAndroidTest, then point the CLI at the directory Gradle already pulls off the device — qf <project> collect app/build/outputs/connected_android_test_additional_output — with no adb command on API 29 and up. Without the reporter: run the same Gradle task and upload the XML it writes, qf <project> collect app/build/outputs/androidTest-results/connected/**/*.xml --format espresso. Either way the CLI attaches your Git branch and commit automatically.

Does Qualflare detect flaky Espresso tests?

Yes, and with the native reporter it is direct rather than inferred: the reporter records every attempt at a test, so a rerun that eventually passed arrives marked flaky with each failed attempt still attached. On the JUnit-XML path that metadata does not exist — the format has nowhere to put it — so Qualflare scores flakiness by watching each test’s pass/fail pattern across launches instead: a test that alternates between green and red across otherwise-identical runs gets flagged, even though no single XML file says “flaky.”

Setup reflects the Qualflare CLI (docs.qualflare.com) and Android/AndroidX Test documentation as of August 2026. Published 14 August 2026. Written by İbrahim Süren, founder of Qualflare.