EarlGrey CI Integration: Running iOS UI Tests in GitHub Actions and Xcode Cloud
EarlGrey tests run with xcodebuild test like any XCTest suite. The challenge is iOS simulator availability, macOS runner requirements, and collecting test artifacts from CI.
Prerequisites
EarlGrey requires:
- macOS runner (not Linux — iOS simulators only run on macOS)
- Xcode installed
- An iOS simulator available
GitHub Actions
Basic Setup
# .github/workflows/ui-tests.yml
name: EarlGrey UI Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
ui-tests:
runs-on: macos-14 # Use macos-14 for Xcode 15.x
steps:
- uses: actions/checkout@v4
- name: Select Xcode version
run: sudo xcode-select -switch /Applications/Xcode_15.2.app
- name: Show available simulators
run: xcrun simctl list devices available
- name: Run EarlGrey tests
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' \
-resultBundlePath TestResults.xcresult \
| xcpretty --report junit --output test-results.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
TestResults.xcresult
test-results.xmlAvailable macOS Runners and Xcode Versions
| Runner | Xcode versions |
|---|---|
macos-14 |
Xcode 15.x |
macos-13 |
Xcode 14.x, 15.x |
macos-12 |
Xcode 13.x, 14.x |
Check the current GitHub-hosted runner docs for current versions.
Caching Dependencies
For CocoaPods:
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Install CocoaPods
run: pod installFor Swift Package Manager, Xcode caches packages automatically. No additional caching step needed.
Parallel Testing
EarlGrey tests run in a single simulator by default. To parallelize across multiple simulators:
- name: Run tests in parallel
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-parallel-testing-enabled YES \
-maximum-parallel-testing-workers 3 \
-resultBundlePath TestResults.xcresultEarlGrey tests are safe to parallelize — each simulator instance is independent.
xcpretty for Readable Output
Raw xcodebuild output is verbose. xcpretty formats it:
- name: Install xcpretty
run: gem install xcpretty
- name: Run tests
run: |
set -o pipefail
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' \
| xcpretty --report junit --output test-results.xmlThe set -o pipefail ensures the step fails if xcodebuild fails, even when piped through xcpretty.
Xcode Cloud
Xcode Cloud runs natively on Apple infrastructure and has first-class support for iOS simulators.
ci_scripts Setup
Create ci_post_clone.sh in your repo:
#!/bin/sh
# ci_post_clone.sh — runs after Xcode Cloud clones your repo
set -e
# Install CocoaPods if needed
if [ -f "Podfile" ]; then
gem install cocoapods
pod install
fixcodebuild in Xcode Cloud
Xcode Cloud configures the build environment automatically. In the workflow editor:
- Add a Test action
- Select your UI test scheme
- Choose simulator destination
- Enable parallel testing if needed
EarlGrey's in-process architecture works transparently with Xcode Cloud's test infrastructure.
Result Bundles and Artifacts
.xcresult bundles contain screenshots, test logs, and coverage. Parse them:
# List test results from bundle
xcrun xcresulttool get --format json --path TestResults.xcresult
# Export screenshots
xcrun xcresulttool export \
--type directory \
--path TestResults.xcresult \
--output-path exported-resultsFor human-readable HTML reports, use xchtmlreport:
gem install xchtmlreport
xchtmlreport -r TestResults.xcresultCollecting Screenshots on Failure
EarlGrey saves screenshots on test failure automatically. The screenshots end up in the .xcresult bundle. Extract them:
- name: Extract failure screenshots
if: failure()
run: |
xcrun xcresulttool export \
--type directory \
--path TestResults.xcresult \
--output-path failure-screenshots
- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: failure-screenshots
path: failure-screenshots/Common CI Failures
"Unable to find a destination matching the provided destination specifier"
The specified simulator doesn't exist on the runner. List available simulators:
xcrun simctl list devices available | grep iPhoneUse a simulator that's actually on the runner, or use name=Any iOS Simulator Device to let Xcode choose.
"Test session exited with unhandled exception"
EarlGrey's EarlGreyApp framework isn't linked to the app target. Verify the build phase includes it for the app target, not just the test target.
Tests pass locally but fail in CI (timeout)
CI machines are slower. Increase the EarlGrey timeout:
// In your test setUp
GREYConfiguration.shared.setValue(60.0,
forConfigKey: kGREYConfigKeyInteractionTimeoutDuration)Simulator not booting in time
Add a boot wait:
# Boot simulator explicitly before running tests
xcrun simctl boot "iPhone 15"
xcrun simctl bootstatus "iPhone 15" -bTest Scheme Configuration
For CI, configure your test scheme to:
- Run tests in random order — catches test ordering dependencies
- Gather coverage — useful for tracking test quality
- Enable address sanitizer — catches memory issues in CI before production
In Xcode scheme editor: Product → Scheme → Edit Scheme → Test → Options.
For command line:
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' \
-enableCodeCoverage YES \
-testOrder random \
-resultBundlePath TestResults.xcresult