Skip to content

XCTest & XCUITest test reporting

A practical guide to reporting iOS test results: why XCTest and XCUITest don’t produce JUnit-XML on their own, how to convert the .xcresult bundle that Xcode actually writes, CI patterns for macOS runners, and how to add hosted, historical analysis — AI failure clustering, flaky-test scoring, and per-launch risk — on top with Qualflare.

Worth saying up front: Qualflare doesn’t run your XCTest or XCUITest suite. It’s a results and observability layer, not a simulator or device farm — it ingests whatever results file your existing CI, simulator run, or device cloud already produced.

How XCTest and XCUITest results actually work

Pipeline diagram: xcodebuild test produces an .xcresult bundle, converted to JUnit-XML by xcresultparser, uploaded with qf collect --format xctest, landing in Qualflare for AI clustering, flaky scoring, and risk

XCTest is Apple’s unit- and integration-testing framework; XCUITest is the UI-testing layer built on top of it for driving a running app through the accessibility tree. Both run the same way — through Xcode’s build system, via xcodebuild test — and both produce the same output format: an .xcresult bundle, a binary, database-like package, not XML.

# Run the suite and produce a result bundle
xcodebuild test \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 15' \
  -resultBundlePath TestResults.xcresult

That bundle is not something most CI reporting tools — or Qualflare — can read directly. Apple’s own xcresulttool is the sanctioned way to pull data out of it, but the interface changed under Xcode 16: the long-standing get object subcommand was deprecated and now requires a --legacy flag, replaced by a new get test-results subcommand family ( summary, tests, activities). Developers reported rough edges during the transition — notably an initially unpublished schema for the new JSON output, raised on Apple’s developer forums — and it’s a reminder that anything built against the old interface needs to keep up:

# Xcode 15 and earlier — deprecated in Xcode 16 (needs --legacy now)
xcrun xcresulttool get object --legacy --path TestResults.xcresult --format json
# Xcode 16+ — the replacement subcommand family
xcrun xcresulttool get test-results summary --path TestResults.xcresult
xcrun xcresulttool get test-results tests --path TestResults.xcresult

Either way, what comes out is JSON in Apple’s own schema — still not the JUnit-XML that CI dashboards and platforms like Qualflare consume. That gap is this page’s central topic.

Converting .xcresult to JUnit-XML: what to actually use

For years, the default converter was the fastlane-community/trainer Ruby gem. It isn’t a safe recommendation anymore: its last release shipped in 2019, and its commit history shows nothing but a dependency bump since November 2021. It predates the Xcode 16 xcresulttool changes above entirely, so it’s 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 reads an .xcresult bundle directly and writes JUnit-XML in one step — no intermediate JSON, no schema wrangling:

# Convert the .xcresult bundle straight to JUnit-XML
xcresultparser -o junit TestResults.xcresult > junit.xml

That junit.xml is what you hand to any JUnit-XML-consuming tool — Qualflare included. It’s installable via Homebrew (brew install a7ex/homebrew-formulae/xcresultparser) or as a prebuilt binary from its GitHub releases, so adding it to a macOS CI runner is a one-line step.

CI patterns: GitHub Actions, Xcode Cloud, Bitrise-style CI

GitHub Actions (macOS runners). Run the suite, convert the bundle, upload — always, even on failure, since a failing run is the one you most need reported:

# .github/workflows/ios-tests.yml
ios-tests:
  runs-on: macos-14
  steps:
    - uses: actions/checkout@v4
    - name: Run XCTest / XCUITest suite
      run: |
        xcodebuild test \
          -scheme MyApp \
          -destination 'platform=iOS Simulator,name=iPhone 15' \
          -resultBundlePath TestResults.xcresult
    - name: Convert .xcresult to JUnit-XML
      if: always() # produce a report even when tests fail — that's the point
      run: |
        brew install a7ex/homebrew-formulae/xcresultparser
        xcresultparser -o junit TestResults.xcresult > junit.xml
    - name: Upload results to Qualflare
      if: always()
      run: qf myapp collect junit.xml --format xctest

Xcode Cloud. Xcode Cloud already produces an .xcresult bundle for every workflow and exposes its path via the CI_RESULT_BUNDLE_PATH environment variable in a post-action script — convert and collect from there:

# ci_post_xcodebuild.sh — Xcode Cloud post-action script
# Xcode Cloud already writes an .xcresult bundle to $CI_RESULT_BUNDLE_PATH
xcresultparser -o junit "$CI_RESULT_BUNDLE_PATH" > junit.xml
qf myapp collect junit.xml --format xctest --branch "$CI_BRANCH" --commit "$CI_COMMIT"

Bitrise and similar mobile CI. Same shape: an Xcode Test step (or a raw xcodebuild test -resultBundlePath script step) produces the bundle, a script step installs and runs xcresultparser, and a final step runs qf collect on the resulting XML. The principle holds across every platform: run the suite, convert the bundle, upload the XML — in that order, on every run.

Common XCTest/XCUITest reporting problems (and fixes)

None of this is a niche complaint — mobile build instability is measurably rising. The Bitrise Mobile Insights Report 2025 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. The issues below are the specific, iOS-flavored version of that trend.

  • No .xcresult bundle at all after a crash. If the test process crashes mid-run — a segfault in the app under test, an XCUITest host-app timeout — Xcode can fail to finalize the result bundle, leaving nothing for xcresultparser to read. Check the job for a non-zero xcodebuild exit combined with a missing bundle path, and treat it as its own failure signal rather than a silent gap in the report.
  • Simulator boot-time variance inflates or times out CI runs. Simulator boot time on shared CI runners is inconsistent — cold caches, resource contention with parallel jobs — and a suite that boots the simulator per-target can lose minutes to variance alone. Boot and warm the simulator once before the test step, and give xcodebuild test a generous -resultBundlePath-writing timeout so a slow boot doesn’t get mistaken for a hung test.
  • XCUITest element queries are flaky by nature. XCUITest waits on the accessibility tree, and animations, network-dependent view state, or slightly different simulator timing between runs produce intermittent “element not found” failures that have nothing to do with the feature under test. This is exactly what a flaky-test score computed from history — not a single run’s pass/fail — is for; see “what you get” below.
  • Xcode-version mismatches break bundle parsing. An .xcresult bundle produced by one Xcode version isn’t guaranteed to parse cleanly with a converter built against a different one — a developer running Xcode 15 locally and CI running Xcode 16 (or vice versa) is a common source of “it works on my machine, not in CI.” Pin the Xcode version in CI (xcode-select / xcodes) to match what generated the bundles you’re testing conversion against.
  • The results file never reaches the upload step. The same ordering mistake as any framework: if the conversion or upload step doesn’t run if: always() (or the equivalent “run even on failure” setting on your CI platform), a failing test run — the one you most need visibility into — produces no report at all.

Send XCTest/XCUITest results to Qualflare

Once you have a JUnit-XML file from xcresultparser, uploading it is the same one-line step as every other framework — just tell the CLI the format explicitly, since auto-detection works off JUnit-XML’s own structure and an explicit --format keeps the launch labeled correctly:

# Upload the converted JUnit-XML — tell the CLI the format explicitly
qf myapp collect junit.xml --format xctest

Worth being precise about what that flag does: --format xctest tells the CLI to label the launch as XCTest for the dashboard and filters — it routes through the exact same JUnit-XML-compatible ingestion path as pytest, JUnit (Java), or any other framework. Qualflare has no bespoke .xcresult parsing logic; the conversion step above is what does the real work, same as it would for any other results file. 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 .xcresult data

  • AI failure clustering. When a backend change breaks 20 XCUITest flows across screens, Qualflare groups them by root cause so you triage a handful of clusters instead of 20 stack traces.
  • Flaky-test scoring from history. Because XCUITest’s element-query timing issues rarely show up as a clean pass/fail in a single run, Qualflare scores each test’s flakiness across CI runs — separating genuinely unstable tests from one-off simulator hiccups.
  • 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.
  • History & trends. Pass rate and flakiness over time across branches and Xcode versions — the aggregation a per-run .xcresult bundle can’t give you on its own.
  • Context & defects. Failures keep their names and messages from the converted JUnit-XML, and you can spin up a defect straight from a failing run.

Raw .xcresult bundle vs Qualflare

  .xcresult bundle Qualflare
Readable without Xcode / xcresulttoolYes
History across CI runsYes
AI failure clustering (root cause)Yes
Flaky-test scoring over timeYes
Local, zero-setup, offlineYes
Full trace, logs & attachments per failureYesVia linked CI artifact

They’re complementary: keep the raw bundle (or an exported .xcarchive) for deep local debugging, add Qualflare for hosted, historical CI observability across every run.

Get AI analysis on your iOS test runs

Start free — convert your .xcresult bundle, run qf collect, and get your first AI analysis in minutes.

Get Started Free

Building out Android coverage too? See Espresso test reporting (JUnit-XML natively, no conversion) or Maestro test reporting for cross-platform E2E flows, and how to get Espresso, XCTest, and Maestro into one dashboard. Chasing intermittent iOS failures? Read fixing flaky mobile tests or the mobile testing complete guide. Qualflare supports XCTest alongside 23+ other frameworks — see the full list, or browse all framework reporting guides. Weighing tools? See how it compares to other test management platforms.

Frequently asked questions

Does Qualflare run my XCTest/XCUITest tests on simulators or real devices?

No. Qualflare is a results-management and observability layer, not a device-execution cloud — it never provisions or touches simulators or real devices. It only needs the results file your run already produced, so it works identically whether that run happened on a local simulator, a self-hosted Mac build machine, or Xcode Cloud.

Does Qualflare convert .xcresult files for me?

No — that’s a separate step you run before uploading. Xcode writes test results as a binary .xcresult bundle, not XML, so you convert it to JUnit-XML first (with a7ex/xcresultparser, or Apple’s own xcresulttool) and then hand the resulting file to the Qualflare CLI. The --format xctest flag tells the CLI to label the launch as XCTest; it routes through the same JUnit-XML-compatible ingestion path as every other framework, with no bespoke .xcresult parsing behind it.

Which iOS test frameworks does Qualflare support?

XCTest and XCUITest, both listed by name in Qualflare’s framework support. Both produce the same .xcresult bundle format from xcodebuild, so the same conversion-then-collect flow covers unit tests (XCTest) and UI tests (XCUITest) alike. Maestro, which also drives iOS UI flows, writes JUnit-XML natively with no conversion step — see the Maestro test reporting guide.

Should I use xcresulttool or xcresultparser to convert my results?

Apple’s own xcresulttool (xcrun xcresulttool get test-results ...) reads an .xcresult bundle but outputs its own JSON schema, not JUnit-XML — you’d still need to transform that JSON yourself. a7ex/xcresultparser does the conversion in one step (xcresultparser -o junit TestResults.xcresult > junit.xml) and is the actively maintained option: latest release 2.2.0, shipped July 2026. The older fastlane-community/trainer gem, once the default choice, hasn’t shipped a release since 2019 or seen a real commit since November 2021 — avoid it.

How do I send XCTest/XCUITest results to Qualflare?

Run xcodebuild test with -resultBundlePath to produce an .xcresult bundle, convert it with xcresultparser -o junit TestResults.xcresult > junit.xml, then upload: qf <project> collect junit.xml --format xctest. The CLI attaches your Git branch and commit and creates a tracked launch.

Does Qualflare detect flaky XCUITest tests?

Yes, from history across runs — the same way it scores flakiness for every JUnit-XML-based framework. XCUITest is particularly prone to element-query timing flakiness, so tracking a per-test flakiness score across CI runs (rather than reading a single run’s pass/fail) is what actually surfaces which UI tests are genuinely unstable.

Setup reflects the Qualflare CLI (docs.qualflare.com), Apple’s XCTest/XCUITest and xcresulttool docs, and a7ex/xcresultparser as of August 2026. Published 14 August 2026. Written by İbrahim Süren, founder of Qualflare.