Mobile app visual testing
Native apps break visually in ways the web does not — dynamic type, notches, dark mode, OS version skew. What XCUITest, Espresso, React Native and the hosted tools each cover, and why device fragmentation is the real cost.
Most visual testing writing is implicitly about the web, and the assumptions do not carry across. A native app has no viewport you control, no CSS to override, an operating system that restyles your controls between versions, and users who set their font size to 200% and expect your layout to cope.
What breaks differently on mobile
- Dynamic Type and font scale. iOS Dynamic Type and Android font scale are user settings that can push text far larger than any designer previewed. Layouts that assume a fixed size break for a substantial and growing share of users.
- Safe areas and device shapes. Notches, dynamic islands, punch-holes, rounded corners and gesture bars all subtract from usable space differently per device.
- OS-rendered controls. Date pickers, alerts, switches and share sheets are drawn by the OS and change appearance between versions. Your screenshot changes and you did not do anything.
- Dark mode. A first-class system setting rather than a nice-to-have, and it doubles your capture matrix.
- Right-to-left layouts. A full mirror of the interface, and where hardcoded leading and trailing constraints surface.
- Keyboard overlay. The software keyboard covers a variable slice of the screen and pushes layouts around.
iOS: XCUITest
The native path. XCUITest can capture screenshots as attachments, but like Selenium on the web it captures and does not compare — there is no baseline or diffing concept built in.
func testPricingScreenAppearance() {
let app = XCUIApplication()
// Pin the things that would otherwise vary between runs.
app.launchArguments += ["-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryM"]
app.launchArguments += ["-AppleLanguages", "(en)"]
app.launch()
let screenshot = app.windows.firstMatch.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
add(attachment)
// Comparison is yours to build, or a service's to provide.
}For comparison you either build it — image diffing plus baseline storage, as in the Puppeteer guide but in Swift — or use a service. The open-source option most teams reach for is swift-snapshot-testing, which does image and view-hierarchy snapshots locally.
Android: Espresso
Same shape. Espresso drives the app and can capture, but comparison is not included. Pin the variables before capturing:
@Test
fun pricingScreenAppearance() {
// Font scale and locale are user settings — pin them or every
// device produces a different baseline.
val config = context.resources.configuration
config.fontScale = 1.0f
config.setLocale(Locale.US)
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.pricing_root)).check(matches(isDisplayed()))
// Capture, then hand off to a comparison step.
}Paparazzi is worth knowing about on Android: it renders Compose and View layouts without a device or emulator, which makes screenshot tests fast enough to run on every pull request. That is a meaningful advantage over emulator-based approaches, at the cost of not exercising the real device rendering path.
React Native
You inherit both worlds: JavaScript layout logic plus two native rendering targets. The same code produces genuinely different output on iOS and Android — shadows, fonts, and text metrics all differ — so a single baseline is not meaningful.
- Storybook plus Loki — Loki targets iOS simulators and Android emulators directly, so a component library gets covered on both.
- react-native-owl — purpose-built visual regression for React Native, running against simulator and emulator builds.
- Maestro or Detox plus a comparison step — flow-level capture, if your risk is journeys rather than components.
The hosted options
Because the matrix is the cost, this is one area where buying is unusually defensible.
| Tool | Mobile coverage | Pricing |
|---|---|---|
| SmartUI (TestMu AI) | Appium, XCUITest, Espresso, plus web | Free 2,000 screenshots; paid not published |
| Percy (BrowserStack) | App Percy, on real devices via BrowserStack | Free 5,000/mo; paid contact sales |
| Applitools | Native mobile, desktop and PDF alongside web | Starter $667/mo billed annually |
| Loki | iOS simulator and Android emulator, Storybook-scoped | Free, MIT |
Real devices versus simulators is the distinction to interrogate. Simulators are cheap, fast and reproducible; they do not reproduce real font rendering, GPU behaviour, or OS-version quirks. If your bugs are the kind that only appear on an actual handset, a simulator-based suite will report green through all of them.
Cutting the matrix sensibly
Everything multiplies, so decide deliberately rather than defaulting to completeness:
- Two devices per platform on every pull request — the smallest and largest screen you support. Most layout breakage appears at the extremes.
- Light and dark on both. Dark mode is a system setting, not an edge case, and it breaks contrast and imagery routinely.
- One large font scale, nightly. Dynamic Type at maximum catches a whole class of defect and is too slow to run every time.
- RTL nightly, if you ship it at all.
- Full device matrix weekly, or before a release.
The determinism work is the same discipline as the web, with mobile-specific variables added: pin font scale, locale, timezone, and animation settings at launch, and stub network responses. How to fix flaky visual tests covers the general principle; on mobile, font scale and locale are the two that catch everyone, because they are user settings that differ silently between devices.
Where we are honest about fit
VisualMax is a web tool. It discovers routes in a running web application and reviews them; it does not test native iOS or Android builds. If your visual surface is mobile-first or mobile-only, the right answer is on the list above — most likely SmartUI or Applitools for breadth, or Loki if your coverage is a React Native component library and you want zero spend.
If you have a web application and native apps, treat them as separate problems with separate tools. Nothing on the market covers both well enough to be worth compromising on either.
Frequently asked questions
How do you do visual regression testing for mobile apps?
Capture screenshots through the native test framework — XCUITest on iOS, Espresso on Android — then compare them against baselines, which neither framework does for you. Either build the comparison layer, use an open-source option such as swift-snapshot-testing, Paparazzi or Loki, or use a hosted service like SmartUI, App Percy or Applitools that covers native platforms.
What makes mobile visual testing harder than web?
The matrix. Web multiplies routes by viewports by browsers; mobile multiplies screens by devices by OS versions by font scales by light and dark mode by locale. On top of that, user settings like Dynamic Type and OS-rendered controls change your screenshots without any code change on your part.
Should mobile visual tests run on simulators or real devices?
Simulators are cheaper, faster and more reproducible, which makes them right for the per-pull-request pass. They do not reproduce real font rendering, GPU behaviour or OS-version quirks, so keep a real-device run before releases if those are where your defects actually appear.