Running the Android Emulator in CI

AndroidCITestingDevOps
Share on LinkedIn

Running the Android emulator in CI used to be a punchline — slow, flaky, and requiring custom KVM setup on bare metal. That's changed. GitHub Actions' android-emulator-runner, Gradle Managed Devices, and snapshot-based boot make emulator CI viable on every PR. I've cut a team's instrumentation test feedback loop from "run locally before merge" to "CI runs it in 8 minutes" by combining AVD snapshots with a well-configured runner. The emulator isn't a perfect substitute for physical devices, but it's good enough for 90% of UI test coverage — and it runs on every commit.

GitHub Actions setup

name: Android Instrumentation Tests

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: 17
          distribution: temurin

      - name: Enable KVM
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm

      - name: Run instrumentation tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          target: google_apis
          arch: x86_64
          profile: pixel_6
          disable-animations: true
          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
          script: ./gradlew connectedDebugAndroidTest

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: app/build/reports/androidTests/

Key flags:

AVD snapshots for speed

Without snapshots, every CI run boots the emulator from cold — 60–90 seconds before your first test runs. Create a snapshot:

# Boot emulator locally
emulator -avd CI_Test -no-window -gpu swiftshader_indirect &
adb wait-for-device
adb shell input keyevent 82  # unlock

# Save snapshot
adb emu avd snapshot save ci_booted

Reference in CI:

emulator-options: -snapshot ci_booted -no-snapshot-save -no-window -gpu swiftshader_indirect

Boot drops to 5–10 seconds. Regenerate the snapshot when you change API level or system image.

Gradle Managed Devices

AGP 7.0+ provides managed devices that handle AVD creation and lifecycle:

android {
    testOptions {
        managedDevices {
            devices {
                create("pixel6Api34", ManagedVirtualDevice::class) {
                    device = "Pixel 6"
                    apiLevel = 34
                    systemImageSource = "google"
                }
            }
        }
    }
}

Run tests:

./gradlew pixel6Api34DebugAndroidTest

Gradle creates the AVD, boots it, runs tests, and tears down. Less YAML configuration than raw emulator-runner, but less control over snapshot optimization.

For deeper GMD configuration, see Gradle Managed Devices for UI tests.

What to test on emulator vs device farm

Test type Emulator CI Device farm
Compose UI tests Every PR Nightly
Navigation flows Every PR Nightly
Screenshot tests (Paparazzi/Roborazzi) Every PR (JVM, no emulator) N/A
Camera/sensor features Skip Pre-release
Performance benchmarks Unreliable Pre-release
OEM-specific behavior Skip Pre-release

Paparazzi and Roborazzi run on JVM without an emulator — use them for visual regression on every PR and reserve emulator time for interaction tests.

Flakiness mitigation

Emulator tests flake more than unit tests. Reduce it:

@get:Rule
val retryRule = RetryRule(retries = 1)  // custom JUnit rule

Self-hosted runners for scale

If you're running 50+ instrumentation tests per PR across multiple API levels, GitHub-hosted runners get expensive and slow. A self-hosted runner with:

...runs the same suite in half the time at lower cost. The trade-off is runner maintenance.

Snapshot and cold boot optimization

AVD snapshots cut emulator boot from 60s to 5s:

- name: AVD snapshot
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 34
    target: google_apis
    arch: x86_64
    force-avd-creation: false
    emulator-options: -no-snapshot-save -no-boot-anim -gpu swiftshader_indirect
    script: ./gradlew connectedDebugAndroidTest

Create snapshot once on runner setup, reuse across CI runs. -no-snapshot-save prevents drift from test pollution.

Parallel sharding

Split instrumentation tests across matrix jobs:

strategy:
  matrix:
    shard: [0, 1, 2, 3]
steps:
  - run: ./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.numShards=4 -Pandroid.testInstrumentationRunnerArguments.shardIndex=${{ matrix.shard }}

Four shards on four runners finish 4× faster than one sequential job — worth the GitHub Actions minute cost for PR feedback latency.

Emulator vs physical device gaps

Emulators miss real-world issues:

Budget Firebase Test Lab or internal device lab for pre-release — emulator CI catches logic bugs, device farm catches hardware integration.

Pair with Android baseline profiles CI to warm startup paths before benchmark runs on emulators.

Common production mistakes

Teams get emulator ci testing wrong in predictable ways:

Shipping emulator ci testing on Android fails quietly when you test only on flagship devices, skip process-death scenarios, or assume minSdk behavior matches latest API docs. Emulator-only validation misses OEM-specific battery optimizations and background execution limits.

Resources

Frequently asked questions

Can you run the Android emulator in CI?

Yes, on Linux CI runners with KVM hardware acceleration enabled. GitHub Actions, GitLab CI, and CircleCI support nested virtualization on their standard Linux runners. Use the reactivecircus/android-emulator-runner action or Gradle Managed Devices to boot an AVD, run instrumentation tests, and shut down.

What is the fastest way to run emulator tests in CI?

Use AVD snapshots — boot the emulator once, save a snapshot after system boot completes, and restore from snapshot on subsequent runs. This cuts boot time from 60–90 seconds to 5–10 seconds. Gradle Managed Devices with persistent AVDs on self-hosted runners is even faster for high-volume CI.

Should I use emulator or physical devices for CI?

Emulators for PR checks — fast feedback on UI logic, navigation, and Compose tests. Physical device farms (Firebase Test Lab, BrowserStack) for pre-release validation on real hardware with GPU, sensors, and OEM-specific behavior. Most teams run emulator tests on every PR and device farm tests nightly or pre-release.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →