
Product news and testing tips.
A typical mobile team runs Espresso for Android, XCTest/XCUITest for iOS, and often Maestro for cross-platform E2E flows — three CI jobs, three result formats, no combined pass-rate or flaky picture across the whole app. All three can reach JUnit-XML: Espresso writes it natively via Gradle, Maestro writes it natively via `--format junit`, and XCTest's `.xcresult` bundles need conversion — the currently maintained tool is `a7ex/xcresultparser`, since `fastlane-community/trainer` stopped shipping releases in 2019. Qualflare's CLI ingests all three, plus Appium's JUnit output, into one dashboard alongside web and API tests.
Key takeaways
- Android, iOS, and cross-platform E2E tests usually run as three disconnected CI jobs — Espresso, XCTest/XCUITest, and Maestro — each producing its own result format with no combined pass-rate or flaky picture.
- Espresso and Maestro both write JUnit-XML natively; only XCTest/XCUITest's .xcresult bundles need conversion.
- The once-standard XCTest converter, fastlane-community/trainer, hasn't shipped a release since 2019 and hasn't been pushed to since late 2021 — the actively maintained tool in 2026 is a7ex/xcresultparser (latest release 2.2.0, July 2026).
- Appium has no official JUnit reporter of its own — the client framework produces it (JUnit/TestNG via Surefire, pytest's --junit-xml, or WebdriverIO's @wdio/junit-reporter) — and Qualflare already ingests that output.
- Mobile build flakiness is a growing, measured problem: Bitrise's 2025 Mobile Insights Report found the share of teams experiencing flakiness grew from 10% to 26% between 2022 and 2025.
- Qualflare's CLI (qf <project> collect <results-file>) auto-detects all of these JUnit-XML-compatible formats into one dashboard, whether the tests ran on a local emulator, a self-hosted device farm, or a third-party device cloud — it never touches execution.
A mobile team running Espresso for Android and XCTest/XCUITest for iOS has two CI jobs and two result formats that can’t see each other — add Maestro for cross-platform E2E flows and it’s three. Each produces a report nobody combines, so there’s no single pass rate, no cross-platform flaky picture, and no way to answer “is this release healthy?” without opening three dashboards. The good news: all three can reach JUnit-XML. Espresso and Maestro write it natively; XCTest’s .xcresult bundles need one conversion step, currently best handled by a7ex/xcresultparser. Once every framework speaks JUnit-XML, Qualflare’s CLI merges them into one dashboard.
Why mobile teams end up with three disconnected test reports
Android and iOS are built on different toolchains by design, so their test runners were never going to share a reporter. Espresso tests run through Gradle and AndroidX Test. XCTest and XCUITest run through Xcode’s own build system. Maestro, adopted specifically to write one E2E flow that drives both platforms, runs as a third, separate CI step with its own report. Add an Appium suite for legacy device coverage and that’s four independent pipelines, four independent artifacts, and — this is the part that actually costs teams — no combined view of whether the app, as a whole, is shipping safely.
This isn’t a hypothetical fragmentation problem. Mobile build instability is measurably rising: Bitrise’s 2025 Mobile Insights Report found the share of teams experiencing any test flakiness grew from 10% to 26% between 2022 and 2025 — and the same report found teams using monitoring tools see 25% fewer flaky reruns. A flaky Espresso test and a flaky XCUITest test that share a root cause — a shared backend staging environment, say — look like two unrelated problems when nobody’s watching both platforms at once. This post covers the reporting layer specifically; for the broader picture of Android/iOS test management, see the mobile testing complete guide.
How Espresso results reach JUnit-XML
Espresso needs no conversion step at all. Running ./gradlew connectedAndroidTest uses AndroidX Test’s AndroidJUnitRunner, which writes JUnit-XML directly as part of the test run:
./gradlew connectedAndroidTest
# XML results land at:
# module/build/outputs/androidTest-results/connected/*.xml
That path is documented behavior of the Android Gradle plugin’s command-line test tasks, not a side effect you have to configure. Any CI job that runs connectedAndroidTest (or connectedCheck) already has a JUnit-XML file sitting in build/outputs when the task finishes — zero extra tooling.
How XCTest and XCUITest results reach JUnit-XML
iOS is the one platform in this stack with a real conversion step. Xcode writes test results as an .xcresult bundle — a binary, database-like package, not XML — via xcodebuild test -resultBundlePath TestResults.xcresult. To get JUnit-XML out of it, you need a converter.
For years the default answer was fastlane-community/trainer. It isn’t anymore: its last release shipped in 2019, and its GitHub history shows no real commits since November 2021 — a single dependency bump, then nothing. Xcode 16 also changed xcresulttool itself, deprecating the old get object subcommand in favor of a new get test-results family (summary, tests, activities) — which is exactly the kind of change an unmaintained wrapper won’t track. Recommending trainer in 2026 means recommending a tool built against a CLI interface Apple has since replaced.
The actively maintained alternative is a7ex/xcresultparser — latest release 2.2.0, shipped July 2026, with releases landing every few months. It parses an .xcresult bundle directly and writes JUnit-XML:
xcodebuild test -scheme MyApp -resultBundlePath TestResults.xcresult
xcresultparser -o junit TestResults.xcresult > junit.xml
That junit.xml is what you hand to any JUnit-XML-consuming platform, Qualflare included.
How Maestro results reach JUnit-XML
Maestro, like Espresso, needs no conversion. Its own documentation on test reports states plainly that “JUnit is the standard for CI/CD integration and for test reporting” — you get it with one flag:
maestro test --format junit --output report.xml .maestro/
That’s a single command producing a standard JUnit-XML file for a Maestro flow suite that already exercises both Android and iOS builds of the same app — genuinely useful for a cross-platform E2E layer sitting above the two native unit/UI suites.
What about Appium?
Appium is a different case: it’s a WebDriver-protocol automation server, not a test framework, so it has no official JUnit reporter of its own. Reporting comes from whatever client library and test framework drive the Appium session:
- Java + JUnit/TestNG — the Maven Surefire plugin writes JUnit-XML to
target/surefire-reports/the same way it would for any JUnit suite. - Python + pytest —
pytest --junit-xml=report.xmlproduces standard JUnit-XML. - JavaScript + WebdriverIO — the official
@wdio/junit-reporterpackage writes it.
If your Appium suite already emits JUnit-XML through any of these — and most CI-integrated Appium suites do, since Surefire, pytest, and wdio’s reporter are the default choice for their ecosystems — Qualflare already ingests it. There’s no Appium-specific integration to build.
Android, iOS, and Maestro: native JUnit-XML vs. conversion required
| Framework | Native JUnit-XML? | Recommended path | Tool / command |
|---|---|---|---|
| Espresso (Android) | Yes — native | ./gradlew connectedAndroidTest writes XML directly | AndroidX Test’s AndroidJUnitRunner |
| XCTest / XCUITest (iOS) | No — needs conversion from .xcresult | Convert the .xcresult bundle | a7ex/xcresultparser (xcresultparser -o junit) |
| Maestro | Yes — native | maestro test --format junit writes XML directly | Built into the Maestro CLI |
| Appium (Java) | Indirect — via the client framework | JUnit/TestNG through Maven Surefire | target/surefire-reports/*.xml |
| Appium (Python) | Indirect — via the client framework | pytest | pytest --junit-xml=report.xml |
| Appium (JavaScript) | Indirect — via the client framework | WebdriverIO | @wdio/junit-reporter |
Wiring it into one CI pipeline
Once every job produces a JUnit-XML file, the CI wiring is the same four steps regardless of platform:
- Run each suite as it already runs today —
./gradlew connectedAndroidTestfor Android,xcodebuild test -resultBundlePath ...for iOS,maestro testfor cross-platform flows. - Convert only where needed — XCTest’s
.xcresultoutput throughxcresultparser; Espresso and Maestro already need nothing. - Point the CLI at each results file —
qf myapp collect <results-file>, once per job, using the same project name across all three. - View the combined dashboard — pass rate, flaky tests, and failures across Android, iOS, and your E2E layer land in the same Qualflare project instead of three separate reporters.
In a GitHub Actions pipeline, that’s three parallel jobs each ending in the same qf collect step:
jobs:
android:
runs-on: ubuntu-latest
steps:
- run: ./gradlew connectedAndroidTest
- run: qf myapp collect app/build/outputs/androidTest-results/connected/*.xml
ios:
runs-on: macos-14
steps:
- run: xcodebuild test -scheme MyApp -resultBundlePath TestResults.xcresult
- run: xcresultparser -o junit TestResults.xcresult > junit.xml
- run: qf myapp collect junit.xml
maestro-e2e:
runs-on: macos-14
steps:
- run: maestro test --format junit --output maestro-report.xml .maestro/
- run: qf myapp collect maestro-report.xml
Three CI jobs, three commands, one myapp project on the other end.
One dashboard for Android + iOS + everything else
This is the same multi-framework aggregation problem this blog has covered for Playwright, Jest, and pytest — mobile just adds two more formats to the pile. The CLI auto-detects JUnit-XML-compatible results with no per-framework adapter to write, the same zero-config approach documented for Qualflare’s 23+ supported frameworks, which already lists Espresso, XCTest, and Maestro by name alongside Playwright, Jest, and pytest. That’s what turns per-framework reporting into real test observability — analysis across results, not just a shared viewer for them.
The payoff is the single pane of glass argument applied to mobile specifically: a flaky pattern that shows up in both the Espresso suite and the XCUITest suite — a shared staging API, a shared test-account pool — is invisible if nobody ever looks at both reports side by side, and obvious the moment flaky scoring runs across the combined data. The same project can hold your web and API JUnit-XML results too, so “release health” stops meaning four dashboards and starts meaning one pass rate, one flaky list, and one set of defects for the whole product — not just the mobile half of it. For the fuller case on what that observability layer is and isn’t, see what mobile test observability actually means.
Qualflare doesn’t run your tests — on any device, anywhere
Worth stating plainly, because it’s easy to conflate with device-execution tools: Qualflare never touches test execution. It doesn’t provision emulators, simulators, or real devices, and it doesn’t schedule or run your test suite. It only needs the results file a run already produced — which means it works identically whether that run happened on a developer’s local emulator, a self-hosted device lab, or a third-party device cloud like BrowserStack, Sauce Labs, or Firebase Test Lab. Swap execution environments and nothing about the ingestion step changes: same file, same qf collect command, same dashboard.
Start free with Qualflare — collect Espresso, XCTest, and Maestro results into one project and see Android, iOS, and everything else on one pass-rate and flaky-test dashboard.
Frequently asked questions
Does Espresso need any conversion to produce JUnit-XML?
No. Running ./gradlew connectedAndroidTest uses AndroidX Test’s AndroidJUnitRunner, which writes JUnit-XML directly to module/build/outputs/androidTest-results/connected/ — no converter, plugin, or extra step required.
Why do XCTest and XCUITest results need converting, and what should I use in 2026?
Xcode writes XCTest/XCUITest results as .xcresult bundles, a binary format, not XML — so they need a converter to become JUnit-XML. The once-standard tool, fastlane-community/trainer, hasn’t shipped a release since 2019. The actively maintained alternative is a7ex/xcresultparser (latest release 2.2.0, July 2026), which parses an .xcresult bundle and outputs JUnit-XML directly: xcresultparser -o junit TestResults.xcresult > junit.xml.
Does Maestro produce JUnit-XML natively?
Yes. Running maestro test --format junit --output report.xml <flow> writes a JUnit-XML report directly — no conversion step, the same as Espresso.
Does Qualflare support Appium test results?
Yes, indirectly through whichever client framework your Appium suite already uses. Appium itself has no official JUnit reporter — a Java+JUnit/TestNG suite gets one via Surefire (target/surefire-reports/), a Python+pytest suite via pytest --junit-xml=, and a JS+WebdriverIO suite via the official @wdio/junit-reporter. If your suite already emits JUnit-XML through one of these, Qualflare’s CLI already ingests it.
Does Qualflare run my mobile tests or execute them on real devices?
No. Qualflare is a results-management and observability layer, not a device-execution cloud — it never touches real devices, simulators, or emulators. It only needs the results file your test run already produced, so it works identically whether that run happened on a local emulator, a self-hosted device farm, or a third-party device cloud like BrowserStack, Sauce Labs, or Firebase Test Lab.
Can I see Android, iOS, and web/API test results in one dashboard?
Yes. Point the CLI at each framework’s results file — Espresso’s XML, the converted XCTest JUnit-XML, Maestro’s JUnit-XML, and any web/API JUnit-XML you already collect — and they land in the same Qualflare project, giving one pass-rate, flaky-test, and defect picture across the whole app instead of one dashboard per platform.
Sources
Related Posts

