Emulator, Simulator, or Real Device: Picking the Cheapest Environment That Can See the Bug
A checkout crash shows up on a two-year-old Samsung running Android 12, reproduces on no emulator in the office, and disappears on the flagship everyone tests with. The instinct is "test on more real devices"; the better habit is to ask, for each property under test, what is the cheapest environment that can observe it at all? Because a wrong answer in either direction is expensive: five physical phones running your full suite will not finish before lunch, and a simulator will happily certify battery life it does not have.
Three environments, three different illusions:
| Environment | What it really is | Trust it for | Never trust it for |
|---|---|---|---|
| Android emulator | full system image under QEMU — real kernel, real framework | UI flow, intents, permissions, API calls | battery, thermal throttle, radio, OEM quirks |
| iOS simulator | the same OS binaries running natively on your Mac — not an emulator | layout, navigation, logic, fast feedback | any hardware property; it shares the Mac's CPU |
| Physical device | the actual stack | sensors, GPS, camera, push, Doze, performance | cheap parallelism; you own and cordon each unit |
The iOS distinction is the one that trips teams: an iOS simulator runs the same kernel and libraries as the device, so it boots in seconds and gives correct behavior — but it has no modem, no battery controller, and no thermal envelope, which is why a "100% passing on simulator" suite can ship an app that overheats. The Android emulator is closer to real (it runs an actual system image and, on x86 hosts, translates ARM libraries), yet it still reports a synthetic battery that never drains and a radio that never drops to one bar.
So split the suite by what it claims to prove. Functional flows run headless in the emulator on every pull request — cheap enough to parallelize, and cheap enough that nobody hesitates before running them:
avdmanager create avd -n ci_pixel7 -k "system-images;android-34;google_apis;x86_64"
emulator -avd ci_pixel7 -no-window -no-audio -gpu swiftshader_indirect &
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 2; done'
adb install -r app-debug.apk
A cold boot lands around 45–90 seconds; the same suite on shared physical hardware costs minutes per device plus queueing, and a shared farm slot is billed while your tests wait for the previous job. Everything the environment fakes runs on real hardware on a schedule: battery drain over a 30-minute playback, thermal throttling during a 10-minute map session, GPS movement, camera permissions in daylight, background sync surviving Doze — nightly or weekly, because that is the budget they need. Reserve owned devices for the paths that hurt most: your top three models by install count, running a 15-minute smoke of login, purchase, and notification receipt. The rule of thumb interviewers accept: emulators for breadth of logic, real devices for truth about hardware and the OS vendor's power management. That split is also what makes automation worth writing at all, which is Mobile Automation with Appium.
The Lifecycle Suite: Install, Upgrade, Permissions, and Interruptions
Most mobile regressions are not in your screens — they are at the edges of the app's life: the install, the upgrade, the permission dialog, and the moment something interrupts. A suite that launches the app and taps through three flows proves none of it, which is why these four groups get named, scheduled, and automated first.
Clean install. Fresh install, first launch, no account, no cached state: onboarding must complete, and the app must not crash when the database is created for the first time. Upgrade in place is the higher-risk sibling and the one teams skip:
adb install -r old-2.9.4.apk # install the previous release, run its onboarding
# create data: log in, save a draft, change a setting
adb install -r new-3.0.0.apk # -r keeps user data — this is a real upgrade, not a reinstall
# assert: still logged in, draft intact, settings preserved, no crash on first launch
If the new build migrates the local database, this is the only test that will notice the migration fails — and users who lose their drafts on update do not file bugs, they leave one-star reviews. Test upgrade from every still-supported release, not just the latest one, because a user on 2.7 who jumps straight to 3.0 exercises a different migration path than one on 2.9.
Permissions. Four states, four tests: granted, denied, denied-permanently ("don't ask again" — the app must handle the dead end and route to settings), and first-ask. Drive them without touching the screen:
adb shell pm grant com.acme.shop android.permission.CAMERA
adb shell pm revoke com.acme.shop android.permission.CAMERA
adb shell cmd appops set com.acme.shop CAMERA deny
The classic defect is assuming the grant: the camera screen calls the camera API unguarded and crashes the first time a user says no. Denial must degrade to an explanation, not an exception.
Interruptions. An app on a phone lives in the worst environment in computing — it gets killed, paused, and talked over. Name the scenarios and script them:
| Interruption | How to trigger (adb) | What must survive |
|---|---|---|
| incoming call during checkout | adb emu gsm call 5551234 |
state restored on return |
| airplane mode mid-upload | adb shell cmd connectivity airplane-mode enable |
retry or clear offline message |
| app switcher kill | adb shell am kill com.acme.shop |
relaunch restores from saved state |
| rotation / fold | adb shell settings put system user_rotation 1 |
no lost form input |
| push arrives after force-stop | adb shell cmd notification post -t Acme 'Order shipped' |
deep link opens the right screen |
| low battery / Doze | adb shell dumpsys deviceidle force-idle |
deferred sync happens later |
Each row is a contract with the user: what the app promises to preserve when the platform interrupts it. Notice that half of these are exactly the scenarios automation later has to drive — which is the bridge to the next topic. Finally, none of this is reportable without evidence; that is the third chapter.
The Support Matrix and the Bug Report: Which Devices, and What Evidence Counts
"Test on all devices" is not a strategy; there are tens of thousands of Android models. The product's support matrix is a small, argued document that answers which combinations get tested, and it is derived from exactly three inputs: the declared minimum OS, the newest OS the week it ships, and where your users actually are — the same compatibility reasoning as API Versioning and Compatibility, applied to hardware and OS builds instead of endpoints.
| Axis | Always include | Why |
|---|---|---|
| OS floor | the declared minimum (e.g. Android 10, iOS 16) | you promised it in the store listing |
| OS ceiling | the beta and the newest stable | platform changes break apps silently |
| Hardware spread | one low-RAM/older SoC, one current flagship | performance cliffs live at the low end |
| Form factor | smallest supported, largest, plus tablet/foldable if claimed | layout bugs do not reproduce elsewhere |
| Vendor skin | at least one non-Pixel OEM | aggressive battery managers kill background work |
The reasoning is worth stating in an interview: the minimum OS catches API misuse the newest SDK forgives, and the newest OS catches platform deprecations the minimum never sees. Neither substitutes for the other, and a matrix containing only flagships on the latest OS tests the environment your developers already own. Pull real numbers into the decision — store console telemetry or analytics showing OS-version and model distribution — so "why do we still test Android 12?" is answered with "18% of sessions", not a preference.
Against that matrix, the bug report is a contractual artifact. The one rule: a mobile finding without device model, OS build, and app version is not a finding — it is a rumor, and it will bounce back to triage within a day.
Device: Samsung Galaxy A13 (SM-A135F) ← not "my Samsung"
OS: Android 12, One UI 4.1, build SP1A.210812.016
App: 3.0.0 (build 30014), installed via APK upgrade from 2.9.4
Network: Wi-Fi → Wi-Fi off (airplane mode) during upload
Observed: upload spinner never resolves; app killed after 30s, no error shown
Expected: offline queue message, retried on reconnect
Evidence: logcat.txt, screenrecording.mp4, adb bugreport, screenshot attached
Capture the evidence mechanically, not by hand: adb logcat -d > logcat.txt, adb shell screencap -p /sdcard/f.png && adb pull /sdcard/f.png, adb shell screenrecord /sdcard/flow.mp4, and adb bugreport bugreport.zip for the deep cases. Two details separate usable from useless reports: the app build (versionName and versionCode/build number — "happens on 3.0" is ambiguous across three internal builds) and the path to the bug (install history and whether it reproduced on a clean install or only after an upgrade).
Pair the report with one quantitative measurement where the defect is performance-shaped, because "feels laggy" cannot be triaged: startup time via adb shell am start -W -n com.acme.shop/.MainActivity (reports TotalTime in milliseconds), frame jank via adb shell dumpsys gfxinfo com.acme.shop framestats, and memory via adb shell dumpsys meminfo com.acme.shop. Quote the number, the device, and the comparison baseline — "2.4 s cold start on the A13 versus 0.9 s on the Pixel 7" assigns the bug to the low-end hardware path immediately, instead of starting an argument about opinions. Numbers attached to a named device close arguments; adjectives start them.
A matrix that says where to test and a report format that says what happened together convert mobile testing from folklore into a repeatable practice — one a rotating team member can pick up without a week of context.
Quiz
1. Why can an iOS simulator's battery and thermal results never be trusted, even when every test passes?
2. Which pair of environments must every support matrix include, and why do they catch different defects?
3. How do you test an Android upgrade path without losing the user's data?
4. A bug report says 'app crashes on checkout, happens on my Samsung'. What is missing, and why does it matter?
Flashcards
Question
Emulator vs simulator vs physical device — one line each.
Click to reveal answer
Answer
Android emulator: full system image under QEMU, real kernel — good for functional/intent/permission tests, fake battery and radio. iOS simulator: same OS binaries running natively on the Mac, emulates no hardware. Physical device: the only place sensors, GPS, camera, push, Doze, thermal, and OEM power managers are real.
Question
The four lifecycle groups every mobile suite must contain.
Click to reveal answer
Answer
Clean install (fresh DB, onboarding), upgrade in place (`adb install -r` with data retained, from *every* supported release, verifying migrations and preserved state), permissions (grant / deny / deny-permanently / first ask), and interruptions (call, airplane mode, background kill, rotation, push after force-stop, Doze).
Question
Why does the permission suite include the 'denied permanently' state?
Click to reveal answer
Answer
After 'don't ask again', the system stops showing the dialog, so an app that assumes a grant crashes or dead-ends. The app must detect the state (checkSelfPermission + shouldShowRequestPermissionRationale) and route the user to system settings instead of throwing.
Question
What three inputs define a device support matrix?
Click to reveal answer
Answer
The declared minimum OS (what the store listing promises), the newest OS including beta (platform changes), and real usage telemetry (OS/version and model distribution from analytics). Add hardware tiers and vendor skins because performance and background-kill defects cluster at the low end and in OEM power managers.
Question
The mandatory fields of a reproducible mobile bug report.
Click to reveal answer
Answer
Exact device model (SM-A135F, not 'my Samsung'), OS build incl. vendor skin, app versionName *and* build number, install path (fresh vs upgrade-from), steps, expected vs actual, plus captured evidence: `adb logcat -d`, screencap/screenrecord, and bugreport for deep cases. Missing any of these makes the report a rumor.
Revision Notes
Key Takeaways
- 1. Choose the environment by the property under test: emulators prove logic cheaply, only physical hardware proves power, sensors, and OEM behavior
- 2. iOS simulator ≠ emulator — it runs the Mac's CPU with no modem or battery, so hardware metrics from it are fiction
- 3. Install, upgrade, permissions, and interruptions are where mobile bugs live; launches and happy-path taps are the easy 20%
- 4. Upgrades must be tested with data retained and from every supported release, because migrations only run along that path
- 5. A finding without device model, OS build, and app build is a rumor — capture evidence with adb while reproducing, not from memory
Interview Tips
- • "How do you decide what runs on emulators vs real devices?" — property under test: functional breadth in CI emulators; battery, thermal, GPS, camera, push, Doze, and OEM background managers on hardware on a schedule
- • "Your suite passes on simulator but users report overheating — why?" — the simulator emulates no hardware; state plainly which properties each environment can observe
- • "How would you build a device matrix?" — minimum OS, newest OS, usage telemetry, low-end hardware, one non-Pixel vendor; justify every row with a defect class it catches
- • "How do you test app updates?" — install previous release, create data, `adb install -r` the new build, assert session/state/migration; repeat from every supported release
Cheat Sheet
Mobile Testing Approaches
Environment choice: emulator = breadth of logic on every PR · physical device = hardware truth (battery, thermal, GPS, radio, Doze, OEM kill lists) nightly · iOS simulator shares the Mac's hardware, so it measures no hardware.
emulator -avd ci_pixel7 -no-window -no-audio -gpu swiftshader_indirect &
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 2; done'
adb install -r app-debug.apk
Lifecycle suite:
| Group | Key command / action | Catches |
|---|---|---|
| upgrade | adb install -r old.apk → create data → adb install -r new.apk |
data loss, failed migrations |
| permissions | pm grant / pm revoke / appops set ... deny |
crash on denial, dead-end UI |
| interruption | adb emu gsm call, airplane-mode, am kill, rotation, notification post |
lost state, stuck spinners |
| power | dumpsys deviceidle force-idle |
work that never resumes |
Matrix axes: minimum OS · newest OS (+beta) · low-RAM and flagship · form factors · at least one non-Pixel OEM — driven by store/analytics distribution numbers.
Report contract (mandatory fields): device model + OS build + app versionName/build + install path + steps + expected/actual + evidence (adb logcat -d, screencap, screenrecord, bugreport).