Mobile app testing is the practice of verifying that an iOS or Android app works correctly across the devices, OS versions, and network conditions users will run it on. It covers unit, integration, UI, and end-to-end tests, and the testing happens continuously through a CI/CD workflow so issues get caught before the app reaches the App Store or Google Play.
What is mobile app testing?
Mobile app testing, also called mobile application testing, is how you check that your app does what it's supposed to do on the hardware, OS, and network conditions your users actually have. It's the same idea as testing a web app or a backend service, but with a wider surface area: thousands of device and OS combinations, sensors and cameras, network variability, and a store review process that punishes you for shipping a bug.
The discipline covers everything from a unit test that checks one function in isolation to a UI test that drives a real device and verifies a checkout flow end to end. Most of the work happens automatically in a continuous testing workflow that runs on every commit, with a smaller manual exploratory pass before release for the things automation can't easily catch.
How does mobile app testing work?
Mobile testing happens in layers that run at different stages of the build, fastest checks first.
Tests get written in the platform's native framework, or a cross-platform one if your team prefers a single test suite across iOS and Android. The two big native frameworks are Apple's XCTest umbrella (which includes XCUITest for UI testing) for iOS, and JUnit plus Espresso for Android. Cross-platform options include Appium, Detox, Maestro, and Flutter's integration_test for Flutter apps. Frameworks are only part of the mobile testing tools stack: simulators, real-device farms, and test reporting dashboards make up the rest.
When a developer pushes a commit, a CI/CD workflow runs the test suite automatically against a clean environment. Unit and integration tests usually run on iOS Simulators (for iOS) or Android emulators (for Android) because they're fast and cheap. UI tests run on the same simulators for speed, with a smaller subset of critical-path tests running on real devices via a hosted device farm (Firebase Test Lab, AWS Device Farm, or similar). If anything fails, the workflow stops and the developer who made the change gets notified.
Here's a simplified iOS unit test in XCTest:
import XCTest
class LoginViewModelTests: XCTestCase {
func testValidEmailAccepted() {
let viewModel = LoginViewModel()
XCTAssertTrue(viewModel.isValidEmail("[email protected]"))
XCTAssertFalse(viewModel.isValidEmail("not-an-email"))
}
}
And the equivalent on Android using JUnit:
import org.junit.Test
import org.junit.Assert.assertTrue
import org.junit.Assert.assertFalse
class LoginViewModelTest {
@Test
fun validEmailIsAccepted() {
val viewModel = LoginViewModel()
assertTrue(viewModel.isValidEmail("[email protected]"))
assertFalse(viewModel.isValidEmail("not-an-email"))
}
}
The actual test logic looks similar across platforms. The complexity sits in the layers around it: the build, the device matrix, the test orchestration, the way results get reported back to the developer who pushed the change.
Types of mobile app testing
Mobile teams run several types of tests, each catching a different class of problem. A healthy strategy uses all of them, weighted toward the fast and cheap ones.
Unit tests check a single function, method, or class in isolation. They run in milliseconds, on every commit, and form the base of the testing pyramid. A well-tested codebase has hundreds or thousands of them. iOS teams write them in XCTest (or the newer Swift Testing framework introduced with Xcode 16); Android teams use JUnit.
Integration tests check that several components work together: the data layer talks to the API correctly, the view model and the network client agree on the contract, the database returns what the repository expects. They run in seconds and catch problems unit tests can't see.
UI tests drive the actual app like a user would. Tap a button, fill a field, expect a screen. On iOS this is XCUITest; on Android it's Espresso. Cross-platform teams often reach for Maestro instead. UI tests are slower (seconds to a minute each) but they catch the layout, animation, and accessibility regressions that lower layers miss.
End-to-end tests extend UI testing across the full stack: real backend, real auth, real third-party services. They're the most expensive tests and the closest to actual user behaviour. See our end-to-end testing guide for the full picture.
Performance and load tests measure startup time, memory use, frame rate, and battery impact. Apple's XCTest framework includes built-in performance test support; Android teams typically use Macrobenchmark or custom tooling.
Accessibility tests check that screen readers, dynamic type, and contrast settings all work. XCUITest queries elements through the same accessibility APIs as VoiceOver, so well-written UI tests catch accessibility regressions as a side effect.
Compatibility tests run the app across a device matrix to catch hardware-specific issues. A test that passes on an iPhone 15 simulator can fail on an iPhone SE because of screen size, or on an older device because of memory pressure. Real-device farms let you cover the matrix without owning the hardware.
Manual exploratory testing is what humans still do better than machines: edge cases, unusual flows, and the gut-check before a release. It's not a replacement for automated testing, it's the thing you do once everything automated is green.
Why mobile app testing matters for mobile development
What makes mobile testing different from other kinds of software testing isn't the test types. It's the cost of getting it wrong. A bug shipped to a web app gets hotfixed in minutes. A bug shipped to an iOS app waits behind Apple's review queue and then waits again for users to update. The gap between "we know it's broken" and "users have the fix" can be days, sometimes weeks. Mobile teams test heavily because they have to.
Testing discipline also shapes how fast a team can move. A test suite that catches regressions in minutes lets developers merge with confidence and ship on a weekly or even daily cadence. A weak suite does the opposite: every release needs a long manual QA pass, merges pile up behind it, and the team slows down to avoid breaking things. The build time cost of running tests on every commit pays for itself the first time a broken checkout flow gets caught before submission instead of after.
There's a reputational cost too. Users rarely report bugs; they leave a one-star review or delete the app. Store ratings compound, and recovering from a bad release takes far longer than preventing one.
Mobile app testing best practices
The discipline is roughly the same across teams that do it well. Six practices stand out.
Use the testing pyramid
Many fast unit tests at the base, fewer integration tests in the middle, a focused set of UI and end-to-end tests at the top. Inverting it (lots of slow UI tests, few unit tests) makes the workflow slow and the test suite brittle. The mobile teams that ship reliably keep the ratio honest.
Test on real devices, not just simulators
Simulators are fast and good for catching most regressions, but they don't reproduce hardware quirks: real-camera behaviour, GPU performance on older devices, battery and memory pressure, network conditions. Most teams run unit and integration tests on simulators, then run a critical subset of UI and end-to-end tests on real devices through Firebase Test Lab or a similar farm.
Wait on conditions, not on time
Replace sleep(2) with proper synchronisation. For Espresso, use IdlingResource to tell the test runner when the app is idle. For XCUITest, use waitForExistence(timeout:) or XCTestExpectation. Time-based waits are the most common cause of flaky tests. For more on managing test reliability, see our flaky tests guide.
Quarantine flaky tests aggressively
A flaky test that fails 5% of the time turns one build in twenty into a wasted re-run. Detect flakes automatically, move them out of the main workflow so they don't block merges, and fix them as a priority. Auto-retrying flaky tests is a patch, not a fix.
Mock external dependencies in unit and integration tests
Real network calls, real payment processors, and real push notification services produce flakes you can't control. Mock them at the seam between your code and the third-party so tests run fast, deterministically, and offline.
Make testing part of the definition of done
If code isn't tested, it's not done. Set the expectation in code review, surface coverage on the team dashboard, and don't treat tests as optional polish. Tests that aren't maintained drift out of sync with the code they're supposed to verify, and the team that wrote them stops trusting them.
Mobile app testing vs web app testing
Both verify that an application works, but the constraints around mobile make mobile application testing noticeably different.
The biggest practical difference is the cost of a bug. A web team can hotfix a regression in minutes once they spot it. A mobile team has to rebuild, sign, submit for review, wait for approval, and then wait for users to update. Mobile teams invest heavily in pre-release testing for exactly that reason: the cost of skipping it is much higher.
How Bitrise handles mobile app testing
Bitrise is a CI/CD platform built for mobile teams, and running tests on every build is one of the things it's tuned for. Each Step in your workflow runs in a clean environment on managed Apple Silicon (for iOS) or Linux runners (for Android), so you're testing the artifact that would ship, not the state of someone's laptop.
For iOS, the Xcode Test for iOS Step compiles your app and runs your XCTest and XCUITest suites against the iOS Simulator. You set the scheme (it must be marked Shared in Xcode so Bitrise can access it), pick a destination, and the Step handles the rest. Results land in the Test Reports tab on each build with pass/fail, execution time, and the exact assertion that failed for any red test. UI test failures include the screenshot and a video of the run, so you don't have to leave the build page to diagnose what happened.
For Android, the Android Unit Test Step runs your JUnit tests. The Android Build for UI Testing Step prepares the APK for instrumented tests, and the Virtual Device Testing for Android Step runs them on real devices through Firebase Test Lab. Results surface in the same Test Reports tab and as comments on the pull request that triggered the build.
A simple iOS workflow looks like this:
workflows:
primary:
steps:
- git-clone@8: {}
- xcode-build-for-test@3:
inputs:
- scheme: MyApp
- xcode-test@6:
inputs:
- scheme: MyApp
- destination: "platform=iOS Simulator,name=iPhone 15"
- deploy-to-bitrise-io@2: {}
When a test fails, your build stops by default and the engineer who pushed the change gets notified. Bitrise also gives you test repetition modes at the Step level: retry on failure for transient issues, until failure for hunting race conditions, and until max repetitions for stress testing. Retry on failure is a useful safety net, but don't let it become a way to hide flaky tests. Quarantine those in Bitrise Insights instead, where the Bottlenecks view surfaces tests that pass and fail inconsistently across runs of the same commit.
For the broader picture of how testing fits into a mobile CI/CD workflow, see our mobile CI/CD guide. For deeper config, see the Bitrise docs on running Xcode tests.
See what Bitrise can do for you
Confidently build, test, and ship high-quality mobile apps with Bitrise.
Frequently Asked Questions
What types of mobile app testing are there?
The main types are unit tests (a single function in isolation), integration tests (a few components together), UI tests (driving the app like a user), end-to-end tests (the full stack from UI to backend), performance tests (startup, memory, frame rate), accessibility tests, and compatibility tests across the device matrix. Most teams run all of them at different cadences: unit and integration on every commit, UI tests on a slightly slower cadence, end-to-end and performance tests nightly or pre-release.
Should I test on simulators or real devices?
Both. Simulators (iOS) and emulators (Android) are fast and cheap, so they handle the bulk of unit, integration, and UI test runs. Real devices catch hardware-specific bugs that simulators miss: camera and sensor behaviour, GPU performance on older devices, real network conditions, and OEM-specific Android quirks. Most teams run unit and integration tests on simulators on every commit, then run a smaller set of critical UI and end-to-end tests on real devices via Firebase Test Lab or a similar farm.
What's the best framework for iOS app testing?
For unit and integration tests, XCTest is the default and ships with Xcode. Swift Testing, introduced with Xcode 16, is the more modern choice for new code, with cleaner async support. For UI tests, XCUITest is the standard. Cross-platform teams sometimes use Appium or Detox so the same test suite runs on iOS and Android, accepting that the framework is less iOS-native.
What's the best framework for Android app testing?
JUnit covers unit and integration tests and is the long-standing default. For UI testing, Espresso is the standard for instrumented tests, with Compose UI Testing covering Jetpack Compose specifically. Maestro and Appium are the popular cross-platform options. For most teams, JUnit plus Espresso (or Compose UI Testing) covers everything, with Appium or Maestro added if there's a strong reason to share test logic with iOS.
How often should I run mobile app tests?
Run unit and integration tests on every commit. They're fast enough that there's no reason not to, and they catch most regressions cheaply. Run UI tests on every pull request, ideally in parallel with the unit tests if your CI platform supports it. Run end-to-end and real-device tests on a slower cadence (per pull request, or nightly) because they're more expensive. The general principle is fail fast: catch what you can with cheap checks before paying for expensive ones.
