Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Consulting Services Hire Book Consulting

Mobile App Testing Services in India

Real-device testing for iOS and Android, run by an engineering team in India for CTOs, founders and heads of product in the US, UK, Canada, Australia and New Zealand. We test the things a laptop cannot show you: the two year old handset on a train, the call that arrives mid-checkout, the permission dialog that changed in the last OS release, and the crash report that arrives unreadable.

Why Does a Mobile App Break in Ways a Web App Never Does?

A web app runs in a browser on a machine that is plugged in, on a connection that mostly works, with one runtime you can reason about. A mobile app runs on hardware you did not choose, on an operating system version you did not pick, with a battery the user is anxious about, on a connection that drops when the train goes into a tunnel. The operating system can kill your process while the user is halfway through a form and will not apologise. That is the whole difficulty in one sentence.

Mobile app testing services in India exist because that surface is genuinely large and testing it well takes people who have seen it fail before. Most teams we talk to are not missing test cases. They are missing the environment where the interesting failures happen. Their suite runs on a simulator on a fast Mac on office wifi, and every test passes, and then the store review shows a one star review saying the app freezes when a call comes in.

Here is what that costs you, in the order it usually hurts. Store ratings move slowly up and quickly down, and a bad release drags a rating for months after the bug is fixed. Rollback is not really available: you can halt a rollout, but every user who already updated is on the broken build until they take a new one, and some of them will not. Crash reports arrive obfuscated if nobody uploaded the symbols, so the first day after a bad release is spent trying to read the error rather than fixing it. And app review adds a queue between you and the fix, which turns a two hour patch into a two day one.

The other cost is quieter. Teams that do not trust their mobile release process slow down. They batch changes into bigger releases because each release feels risky, which makes each release riskier, which makes them batch harder. We have picked up apps shipping once a quarter where the engineers were perfectly capable of shipping weekly and simply did not believe the build was safe.

This page is about mobile specifically. Web end-to-end testing, API contract testing, load testing, penetration testing and accessibility auditing are different disciplines with different tools, and we scope them separately for a reason. Mobile has its own physics.

Real Devices or Emulators: When Does Each One Lie to You?

This argument gets treated as a religious question and it is not. Emulators and simulators are excellent at some jobs and actively dishonest at others. The skill is knowing which is which, and the cost of getting it wrong is a suite that is green while your users are unhappy.

What a simulator or emulator is genuinely good for

Fast feedback on logic and layout. If a developer changes a screen and wants to know whether the flow still works, the iOS Simulator boots in seconds, an Android emulator with hardware acceleration is fast enough to run a full suite on every pull request, and neither needs a device to be plugged in, charged, awake or physically present in the same country as the build machine. For unit tests, widget tests, screenshot comparison, deep link handling, most navigation logic and the large majority of business rules, a simulator gives you the same answer a real device would, in a fraction of the time.

They are also the only sane way to test a wide screen-size matrix quickly. Checking that a layout survives a small phone, a large phone and a tablet is a screenshot job, and running it on twenty simulated screen sizes costs almost nothing.

Where the simulator starts lying

The iOS Simulator is not iOS. It is a compilation of your app for the Mac's own architecture, running against a simulated system. Performance is the loudest lie: your app runs on a desktop CPU with desktop memory and no thermal ceiling, so anything you conclude about speed, jank, scroll smoothness or memory pressure is fiction. A list that scrolls beautifully in the simulator can drop frames badly on a four year old handset.

Then there is everything the simulator does not have. Camera and photo capture behave differently. Biometrics are faked. Push notifications work only in a limited form. Bluetooth, NFC, cellular radio, real GPS, the ambient light sensor and the accelerometer are either simulated approximations or absent. In-app purchase behaves differently from the sandbox on a device. Keychain behaviour under device lock differs. And nothing in a simulator will ever tell you what happens when the OS terminates your app to reclaim memory, because the Mac has plenty.

The Android emulator is closer to the real thing, because it runs an actual Android system image. It is still wrong about performance, still wrong about battery, still wrong about thermal throttling, and it is missing the layer that causes a disproportionate share of real Android bugs: the manufacturer's own modifications. Aggressive battery managers on some vendor builds kill background work in ways stock Android does not. Custom permission dialogs behave differently. Vendor keyboards do things to your text fields that the stock keyboard never does.

Where only a real device tells the truth

Anything with a clock or a battery attached to it. Battery drain over a two hour session. Thermal throttling once the device gets warm and the CPU is pulled back. Memory pressure on a device with three or four gigabytes of RAM where the OS is already juggling other apps. Real network behaviour on a real cellular radio, including the reconnection storm when the phone regains signal. Camera, biometric authentication, Bluetooth peripherals, NFC payments and anything else that touches hardware. Store-related flows, including in-app purchase and the review process itself.

There is one more category people forget: the state that only accumulates over time. A device that has been used for a year has a full photo library, thousands of contacts, low free storage, dozens of apps competing for background execution and an install of your app that has been upgraded through six versions rather than installed clean. Fresh emulators never look like this, and a surprising number of production bugs live exactly here.

How we split it in practice

Unit, component and widget tests run on the build machine with no device at all. Fast UI tests and screenshot checks run on emulators and simulators on every pull request, because that is where speed matters most. The device matrix runs on real hardware, in a cloud farm for breadth and on physical handsets for the sensor, battery and long-lived-state cases a farm handles badly. Release candidates always go through real devices before submission, never a simulator alone.

Cloud farms such as Firebase Test Lab, AWS Device Farm, BrowserStack and Sauce Labs give you devices you would not otherwise own, which is the honest argument for them. Their limitation is that sessions are wiped, devices are shared, and the physical environment is a rack rather than a pocket. So they are excellent for breadth and poor for anything about heat, battery or accumulated state. Both halves are needed, and the mix is a decision we make from your app rather than from a preference.

Building a Device Matrix From Your Analytics, Not From a Vendor List

Every device farm publishes a list of popular devices and every one of those lists is wrong for you. Popular in the world is not popular in your app. A fintech app in the UK, a logistics app used by drivers in Texas and a consumer app with a large user base in Indonesia have almost nothing in common in their device distribution, and picking from a generic top twenty means you test devices your users do not own while missing the one that is generating a quarter of your crashes.

The data you need already exists. Google Play Console gives you device and Android version distribution for your actual installs, plus crash and ANR breakdowns by device model. App Store Connect gives you iOS version adoption and device class. Firebase or whichever analytics you run gives you sessions by model. Your crash reporter gives you the far more interesting view: which devices crash disproportionately relative to their share of sessions.

The selection rule we use

Start with devices covering the top slice of your sessions, usually somewhere between sixty and eighty percent, which is normally a smaller number of models than people expect because device distribution has a long tail and a fat head. Then add, deliberately, the things that coverage misses.

The oldest OS version you still officially support, on hardware from that era rather than a new phone downgraded. A low-RAM Android device, because that is where your app gets killed in the background and where cold start times become visible. The smallest screen in your install base, because that is where a layout breaks or a button falls under the keyboard. The newest OS beta, because permission and privacy behaviour changes almost every year and you want to find that in August rather than in a support queue in September. And one device you dislike, from a manufacturer whose battery management is known to be aggressive, because it will find bugs the rest of the matrix will not.

Foldables and tablets only if your data says so. A foldable introduces a genuine class of bug, since folding and unfolding is a configuration change that recreates activities and destroys state that was never saved properly. If foldables are a rounding error in your analytics, ignore them and say so in writing. If they are not, they need a dedicated set of cases.

Why the matrix is not permanent

Device matrices rot. A model that mattered eighteen months ago is now four percent of sessions, a new mid-range Android has quietly become your largest single model, and the OS version floor has moved because your minimum supported version changed. We review the matrix every release cycle against fresh numbers, and we write down what came off and why. This takes twenty minutes and prevents the classic outcome where a team is still carefully testing a phone nobody uses.

One more input people skip: the crash-to-session ratio. If a device is one percent of your sessions and nine percent of your crashes, it belongs in the matrix regardless of its popularity, and it probably deserves an investigation rather than a test case. That is often where a driver bug, a vendor-specific webview or a manufacturer's power management policy is hiding.

Appium, Espresso, XCUITest and Maestro: The Real Trade-Offs

There is no correct answer here, only a correct answer for your team, your app and who is going to maintain the suite in a year. The question we ask first is not which framework is best. It is who owns these tests when the person who wrote them has moved on.

Espresso, for Android

Espresso runs in the same process as your app, which is the whole point. It knows when the main thread is idle and synchronises automatically, so the endless sleep-and-hope pattern that plagues external automation mostly disappears. It is fast, it is stable, and because it is written in Kotlin or Java alongside the app it can reach into the code, seed state, and stub dependencies rather than clicking through six screens to reach the one under test.

Its limits are real. Espresso stops at your app's boundary, so a system permission dialog, the camera app, a browser handoff or another application needs UI Automator alongside it, and the two together are noticeably less pleasant than either alone. Asynchronous work that does not surface through the main thread needs IdlingResource wiring, and teams that skip this end up with the exact flakiness Espresso was supposed to prevent. And it is Android only, so you are maintaining a second suite for iOS regardless.

XCUITest, for iOS

XCUITest is Apple's own framework, integrated with Xcode and XCTest, and it is the only option that will always support the newest iOS on day one. It drives the app through the accessibility layer, which has a useful side effect: if your app is hard to automate with XCUITest, it is usually hard to use with VoiceOver too, and fixing one improves the other.

The catch is that the test runner is a separate process from the app, so unlike Espresso you cannot simply reach into your own objects. State setup happens through launch arguments and environment variables, or through a debug menu you build for the purpose. Tests are slower than Espresso for the same reason. And element queries against a poorly labelled UI become brittle strings that break when a designer changes copy, which is why accessibility identifiers need to be applied deliberately rather than discovered later.

Appium

Appium is the pragmatic choice when you want one suite, one language and one set of people covering both platforms. It speaks the W3C WebDriver protocol, drives Android through the UiAutomator2 driver and iOS through the XCUITest driver, and works against real devices in every major cloud farm. If your QA engineers already know Selenium, the learning curve is short, and the same test code can cover both platforms where the flows are genuinely the same.

The price is speed and synchronisation. Every action is a round trip through a server to the device, so an Appium suite is meaningfully slower than the native equivalents. Because it is outside the app it has no idea when your app is busy, so waits are your problem and implicit waits are how suites become flaky. Appium 2 split drivers and plugins out of the core, which is better architecture but means version alignment between Appium, the driver, the platform tooling and the OS is something you now maintain. And where the two platforms genuinely differ, the shared test collapses into a pile of conditionals, at which point you have one suite that is harder to read than two.

Maestro

Maestro is newer and takes the opposite position: flows are declared in YAML, waiting is implicit and built into the model, and the tool retries and tolerates timing noise by design rather than asking you to configure it. For smoke tests, critical path checks and the set of flows a product manager cares about, it is fast to write and unusually stable, and non-specialists can read and edit a flow file without learning a test framework.

It is deliberately less powerful. Complex conditional logic, deep assertions on internal state and anything requiring real code are awkward or impossible, and it is not the tool for an exhaustive regression suite. We often see the best outcome from a combination: Maestro for the twenty flows that must never break, native frameworks for depth on the platform teams' own code.

How we usually decide

If two platform teams own their own quality and there is no shared QA function, native frameworks win and the duplication is worth it. If a single QA team covers both platforms and the app's flows are largely the same on each, Appium is the honest choice and the speed cost is manageable if the suite is sharded. If the immediate problem is that nobody has any confidence in the release at all, we start with Maestro on the critical path because it delivers a trustworthy signal in days rather than weeks, and build depth afterwards.

What we do not do is rewrite a working suite because we prefer a different tool. An imperfect suite the team maintains is worth more than a beautiful one they abandon.

Testing the Network Your Users Actually Have

Office wifi is the most misleading test environment in software. Requests complete in milliseconds, nothing is ever dropped halfway, and no connection changes type mid-request. Your users are on a bus, in a lift, in a car park, in a rural area with two bars, or on hotel wifi that requires a captive portal login your app has never heard of.

Shaping the connection deliberately

On iOS and macOS, Network Link Conditioner gives you named profiles for 3G, Edge, high latency DNS, very bad networks and lossy connections, and it can be enabled directly on a physical iPhone once the developer profile is installed. On Android, the emulator supports network speed and latency profiles, and adb lets you disable and re-enable data and wifi from a script, which is how you build repeatable connectivity tests rather than pulling a phone in and out of a faraday bag.

For anything more precise, a proxy is the right tool. mitmproxy or Charles sitting between the app and your backend lets you add fixed or jittered latency per endpoint, throttle bandwidth, return a 500 for one specific call while everything else succeeds, truncate a response body halfway through, delay a response beyond the client timeout, and replay a malformed payload the backend produced once in production and has since fixed.

The tests that actually find bugs

Slow network tests are the easy half and rarely the interesting one. What breaks apps is ambiguity. A payment request that reaches the server, succeeds, and then fails to deliver the response, so the client retries and the user is charged twice. A token refresh that fires from three concurrent requests at once because there is no single-flight guard, and two of them get invalidated tokens. A file upload interrupted at eighty percent with no resume, so the user starts again from zero on a metered connection. A request that times out at thirty seconds while the OS backgrounds the app at twenty eight.

So the cases we write are about transitions. Switch from wifi to cellular mid-request and confirm the app recovers rather than hanging on a dead socket. Go fully offline mid-flow and check whether the queued action survives an app restart. Come back online and verify the retry does not stampede, that it backs off, and that it does not replay an action that already succeeded. Kill the connection during authentication and confirm the user lands somewhere sensible instead of a blank screen.

Offline and resume

If your app claims offline support, that claim needs its own test set rather than a note in the spec. The questions are specific. What is written locally and when is it flushed? What happens if the app is force-killed with unflushed writes? If the same record was edited offline on two devices, what is the conflict rule, and is it the rule the product team thinks it is? Does a queued action survive an OS-initiated termination, or only a manual backgrounding? Does the user get told they are offline, or does the app simply show stale data with no indication?

The failure we see most often is silent staleness. The app has no connection, shows cached content with no marker, and the user acts on information that is a day old. That is not a crash and no monitoring tool will flag it, but it produces support tickets that are very hard to reproduce.

Captive portals and hostile networks

Hotel and airport wifi intercepts DNS and returns a login page instead of your API response. If your networking code parses that HTML as JSON and crashes, you have a crash cluster that is impossible to reproduce at the office. It takes one test to cover and almost nobody writes it. Similarly, corporate networks with TLS inspection, proxies that strip headers, and IPv6-only networks all produce failures that look like backend problems and are not.

Battery, Thermal and Memory Behaviour

Nobody files a bug report saying your app drains their battery. They uninstall it, or they leave a review saying the app is a battery hog, and by then the damage is done and the cause is unknown. This category has to be measured deliberately because it produces no crash, no error and no stack trace.

Battery

On Android the method is repeatable. Unplug the device, reset battery stats through adb, run a scripted session of a fixed length doing a realistic mix of activity, then dump the stats and load them into Battery Historian. What comes out is a timeline attributing wakelocks, alarms, wakeups, GPS time, radio activity and CPU time to specific apps. Long partial wakelocks and frequent alarms are the usual suspects. So is a location subscription set to high accuracy that was never downgraded when the screen turned off.

Android's power management is aggressive and testing has to account for it rather than around it. Doze restricts background work when the device is stationary and unplugged. App Standby Buckets change how often an app is allowed to run based on usage. Background execution limits have been tightening for years. If your background sync assumes it can run every fifteen minutes forever, it will not, and the correct test is to force the device into Doze and idle states through adb and see what actually happens to your scheduled work.

On iOS the Energy Log in Xcode's Organizer and the energy template in Instruments give you the equivalent view, and the Metrics organizer reports aggregate battery impact from real users. The constraints differ: background execution is far more restricted, background tasks scheduled through BGTaskScheduler run when the system decides rather than when you asked, and testing them properly means triggering them manually in the debugger rather than waiting.

Thermal

A phone that gets hot throttles its CPU and GPU, and your app gets slower in a way no benchmark on a cool device will show. This matters for video, camera, games, mapping, machine learning inference on device and anything doing sustained work. Both platforms expose the thermal state to the app, so the app can degrade gracefully by lowering a frame rate or a capture resolution, and that degradation path deserves a test of its own.

The test is a soak. Run the heavy path for twenty or thirty minutes on a physical device, watch the frame rate and the thermal state together, and check that the app reduces its own workload rather than continuing to demand what it cannot get. An app that ignores thermal pressure drags the whole phone down with it, and users blame the app they can see.

Memory

Android kills background processes under memory pressure and calls it normal operation. iOS terminates apps that exceed their footprint. Both of these are guaranteed to happen to your app and neither counts as a crash you will necessarily see in your reporting. The relevant test is not whether the app is killed, it is whether the app comes back correctly.

So: open the app, get halfway through a multi-step flow with unsaved input, background it, apply memory pressure by opening several heavy apps or by triggering process death from the tooling, then return. Does the app restore the user to where they were with their input intact, or does it drop them at the home screen having lost twenty minutes of work? On Android this is the saved instance state question and it is failed constantly, because during normal testing the process is never actually killed. Developer options include a setting to keep no activities, which makes this reproducible on demand and belongs in every Android test plan.

Leaks get their own pass. LeakCanary on Android surfaces retained objects during instrumented runs, and Instruments handles the iOS side. The pattern worth watching for is a leak that only manifests after repeated navigation, so the test is to cycle through a screen thirty times and watch the trend rather than the absolute number.

Interruption Testing: The Category Everyone Skips

A phone is a device that interrupts you. That is its primary function. Yet most test plans assume an uninterrupted user progressing calmly from screen one to screen five, and almost no real session looks like that.

The interruption list

An incoming call arrives while the user is on the payment screen. A push notification lands and the user taps it mid-form, navigating away from unsaved input. The user switches to another app to copy a code from an SMS or an authenticator, then comes back. The screen locks on a timer while a scan or an upload is running. The device rotates, or a foldable is opened, recreating the activity. The user drags down the notification shade during a video call. An alarm goes off. Battery saver kicks in at fifteen percent and changes what background work is permitted. Storage runs low and the OS starts asking to free space.

Each of those is a lifecycle event, and each one gives your app a chance to lose state, double-submit, leave a modal orphaned or deadlock waiting for something that was cancelled. The pattern is always the same: something was in flight, the app was suspended, and the code that resumes it was written on the assumption that it would not be.

What we actually run

The interruption suite is scripted rather than improvised. On Android, adb can simulate an incoming call, send the app to the background, revoke a permission, trigger process death, toggle battery saver and change screen orientation, all from a script, which makes the whole set repeatable in CI. On iOS the equivalent is partly simulator-driven and partly manual on a device, and the manual portion is worth the time because it is where the worst bugs are.

The assertions are about state, not appearance. After the interruption, is the user in the same place? Is unsaved input still there? Did the in-flight request complete once, twice or not at all? Is the loading spinner still spinning forever because its completion handler was discarded? Did the payment go through twice because the retry logic did not check idempotency?

Low storage and full devices

A device with almost no free space behaves differently from your test handset with sixty gigabytes free. Writes fail. Cache directories get cleared by the OS without notice. Image downloads silently do not persist. Database writes throw exceptions that nobody handled because they had never seen one. Filling a device deliberately is easy, and the test regularly finds an unhandled write failure that has been in the code for years.

Related: the OS can clear your cache directory at any time and users can clear app data from settings. If your app treats a cached file as guaranteed to exist, or stores an auth token somewhere the OS considers disposable, that is a bug waiting for a busy device.

Permissions Across OS Versions

Permissions are the part of mobile that changes most often and gets tested least. Both platforms have tightened privacy behaviour repeatedly, and the tightening is rarely backwards compatible in behaviour even when it compiles fine. An app that handled permissions correctly two OS versions ago can be quietly broken on the current one.

What actually changed and why it matters

On Android, runtime permissions replaced install-time grants years ago, and the model has kept moving since. Users can grant a permission once rather than permanently. The system auto-resets permissions for apps that have not been opened in a while, so a returning user arrives with permissions your code assumed were granted. Notifications became a runtime permission, which means an app that simply posts notifications now silently posts nothing on newer versions unless it asks. Photo access can be partial, where the user picks specific images and your app sees only those, and code that enumerates the whole library gets an empty or partial result rather than an error.

On iOS, location can be granted as approximate rather than precise, which breaks anything assuming a usable fix. Photo library access can be limited to a selection the user manages. App Tracking Transparency governs access to the advertising identifier, and the prompt has its own rules about when it can be shown. Every permission needs a purpose string in the app's configuration, and a missing one is not a warning, it is a crash on first use and a guaranteed review rejection.

The permission test matrix

For each permission the app requests, we test the full state machine rather than the happy path. Grant it and confirm the feature works. Deny it once and confirm the app explains itself rather than showing a dead button. Deny it permanently and confirm the app routes the user to settings rather than re-prompting into a wall. Grant it, then revoke it in system settings while the app is backgrounded, then return to the app: this is the case that crashes, because the process is killed on some revocations and resumed on others. Grant the reduced form, approximate location or partial photos, and confirm the feature degrades rather than breaks. Then repeat the important ones on the oldest and newest OS versions in the matrix, because the dialog and the behaviour genuinely differ.

The upgrade path deserves its own case. A user who granted a permission three versions ago, on an older OS, then updates both the OS and the app, is in a state you cannot reach by installing fresh. Testing it means building the old version, granting the permission, then upgrading in place. It is fiddly and it is where a real class of bug lives.

What App Store Review Rejects That Testing Can Catch First

Review rejection is expensive in a way that is easy to underestimate. The days in the queue are the smallest part of it. You also lose the release window, the marketing scheduled around it, and a second submission that lands with a different reviewer who has a different question. A meaningful share of rejections are mechanical and preventable, and prevention is a checklist, not a negotiation.

The mechanical rejections

Crashes and obvious bugs during review are, by Apple's own account, among the most common reasons apps get sent back. The reviewer is on a device you did not test, often on a network you did not test, running a build you compiled last night. A crash on launch on an older device is the classic.

Then the access problems. If any part of your app sits behind a login, review needs working demo credentials, and those credentials need to still work on the day of review rather than having expired with a test data reset. If a feature needs specific account state, a subscription, a verified identity or seeded data, the reviewer will not have it unless you provide it. Apps that require hardware the reviewer does not have need a demo video and a written explanation.

Apple reviews on a network configuration that has caught out a lot of apps: connectivity that is not plain IPv4. Networking code that hardcodes IPv4 assumptions, or a third party SDK that does, fails in review while working perfectly everywhere you tested. Purpose strings for every permission, correct handling of account deletion where account creation is offered, and the current requirements around third party sign-in options are all checkable before submission rather than after.

Both stores also enforce moving targets: a minimum target SDK or API level for new submissions, a minimum build toolchain version, and privacy disclosure requirements covering your own code and the SDKs you embed. These change on a schedule, so the right practice is to check the current requirement against the store's own documentation before each release rather than trusting what was true last year. That check is part of our pre-submission pass, not an assumption.

The pre-submission run

Before a build goes to either store, it goes through a fixed pass on a clean device: fresh install, no cached credentials, no developer tooling attached, on a restricted network. Launch it cold. Walk the primary flows. Trigger every permission prompt and read every purpose string as a user would. Confirm the demo account works right now. Confirm every link in the app opens, including the privacy policy and support URLs, because a dead support link is a real rejection reason. Confirm nothing in the build references test environments or internal tooling. Check the app on the largest and smallest supported screens because layout truncation gets flagged.

On the Android side, the Play Console pre-launch report runs your build automatically on a set of real devices and reports crashes, ANRs, accessibility issues and screenshots per device. It is free with an internal test track upload and a genuinely useful second opinion, and a surprising number of teams never open it.

Release Channels, Staged Rollout and Knowing When to Stop

Mobile has no rollback. Once a user has installed version 4.2, they have it until they install something else, and you cannot take it away. What you have instead is control over how many people get it and how fast, and that control is only useful if someone is watching the right numbers while the rollout runs.

The track structure

Google Play gives you internal testing for the team, closed testing for a named group, open testing for anyone who opts in, and production. Apple gives you TestFlight with internal and external groups, where external groups require a review pass of their own. Used properly, a build climbs those steps and each step is a filter: internal catches the obvious, closed catches the environment-specific, open catches the device diversity you did not model.

The mistake is treating the tracks as a formality and promoting a build through all of them in one afternoon. Each stage needs enough time and enough sessions to produce a signal. A build that has been in internal testing for two hours on four devices has told you almost nothing.

Staged rollout and the halt decision

Play lets you release to a percentage of users and increase it in steps, and it lets you halt a rollout entirely, which stops further users receiving it while leaving existing updaters where they are. Apple's phased release distributes an update across a week and can be paused. Both are useful and both are wasted if nobody defines in advance what would cause a halt.

So we write the halt criteria before the rollout starts, as numbers rather than feelings. Crash-free session rate relative to the previous version's baseline. ANR rate on Android against the same comparison. The conversion rate of the one flow that matters most to the business. Store review sentiment in the first day. Google publishes bad-behaviour thresholds for user-perceived crash and ANR rates in the Play Console, which are worth checking against the current documented figures because they have moved before and they affect store visibility, not just your dashboard.

The other half of that decision is who is allowed to make it and how fast they can be reached. A rollout that starts on a Friday afternoon in your timezone, monitored by a team going into their evening, is a decision waiting to be made too late. Where an offshore team is involved this is genuinely an advantage rather than a complication, and it is one of the few places where the timezone gap works in your favour: the first hours of a rollout can be watched by people who are awake for them.

Feature flags, because the store queue is not your friend

The fastest fix for a bad feature is not a new build. It is a flag that turns the feature off for everyone without shipping anything, which takes minutes instead of days. That only works if the flag was in the build before the problem appeared, so risky features get a kill switch as a matter of course, and the kill switch gets tested. A flag that has never been flipped in a real build is not a kill switch, it is a hope.

Crash Reporting and Symbolication

The day after a release is the day your crash reporting either earns its keep or costs you a fortune in guesswork. Most of what determines which one happens was decided weeks earlier, in build configuration nobody was paying attention to.

Making the stack trace readable

Release builds are optimised and obfuscated, so raw crash reports are addresses and renamed symbols. Making them readable requires the mapping artifacts, and the mapping artifacts have to be uploaded for the exact build that crashed. On Android that is the R8 or ProGuard mapping file, plus native debug symbols if you ship any NDK code. On iOS it is the dSYM bundle, which is easy to lose when builds happen on a machine that gets cleaned, or when a build is regenerated and the old dSYM no longer matches.

The rule we enforce is that symbol upload is a build step, not a person's responsibility. If the upload can fail silently, it will, and you will find out at the worst moment. So the pipeline uploads mapping files and dSYMs as part of the release job, and the release checklist verifies that a deliberately triggered test crash from the release candidate appears in the dashboard fully symbolicated before the build goes anywhere. That single check is the difference between a two hour investigation and a two day one.

Reading the data properly

Crashlytics, Sentry and Bugsnag all group crashes into issues, and the grouping is a heuristic that sometimes merges unrelated problems or splits one problem into six. So the first read is always about breadth rather than count: how many users, on which OS versions, on which devices, from which app version, and does the breadcrumb trail show a consistent path into it.

Two things get missed almost universally. The first is ANRs on Android, which are not crashes and appear in a separate place, yet they hurt user experience as much as a crash and count against your app in the store. An ANR happens when the main thread is blocked past the system's tolerance, usually by disk or network work that should not be there, and it is far more common than teams expect. The second is non-fatal error reporting: handled exceptions that the app recovered from but which indicate something is wrong. An app can have an excellent crash-free rate while quietly failing at its main job.

Crash-free session rate and crash-free user rate are different numbers and both are needed. A crash that hits one user fifty times barely moves the user rate and destroys that person's experience. A crash that hits many users once each looks worse in aggregate and may matter less. Read them together, always compared against the previous version rather than as an absolute.

React Native and Flutter: What Cross-Platform Changes

Cross-platform frameworks do not remove platform problems. They add a layer, move the failure modes, and make some of them harder to see. Testing them well means understanding what the framework is doing underneath rather than treating it as a black box that produces two apps.

Flutter

Flutter does not use native views. It draws its own widgets onto a canvas, which is why it looks identical across platforms and why external automation tools struggle with it. An accessibility-driven tool sees a largely empty screen unless the semantics tree is enabled, so an Appium suite written against a Flutter app either fails immediately or ends up driving coordinates, which is the most brittle automation there is.

The sane path is Flutter's own layers. Widget tests are extremely fast and cover a large amount of UI logic without a device at all. The integration_test package runs on real devices and simulators and gives you full app coverage in Dart. Where a flow needs native interaction, a system permission dialog, a notification, a biometric prompt, Patrol extends integration_test to reach outside the Flutter surface, which is the gap that used to force teams into an awkward hybrid.

Flutter-specific things that need testing on their own: platform channel code, because that is real native code with real platform differences and it is where the framework's abstraction stops; the semantics tree itself, since accessibility does not come free when you draw your own widgets; and performance in profile mode on a real device rather than debug mode, where Dart runs interpreted and every measurement is meaningless.

React Native

React Native does render native views, so automation tools can see them, but the synchronisation problem is severe. Work happens across a JavaScript thread and the native side, and an external tool has no idea when the JavaScript is finished. That is the entire reason Detox exists: it is a gray-box framework that knows when the app is idle and waits for it, which removes the sleep-based flakiness that plagues Appium suites on React Native.

Element identification needs discipline. The testID prop maps down to native identifiers on both platforms, but only if it is applied consistently, and retrofitting it across an existing app is dull work that teams postpone until their tests are already brittle. Applying it as a convention from the start costs almost nothing.

Then there is the update mechanism. Over-the-air JavaScript updates are a genuine advantage, since a JavaScript-only fix can reach users without a store queue, and they are also a way to ship an untested bundle to production with no review gate in the way. If you use them, the bundle needs the same testing discipline as a store build and the same staged rollout, and someone needs to have thought about what happens when a bundle and a native binary disagree about what version they are.

Both frameworks share a dependency problem worth naming. A cross-platform app carries a long chain of community packages, each with its own native code, and an OS upgrade can break any of them. Testing against OS betas therefore has to cover the dependency surface, not just your own code, and the fix is often waiting for a package maintainer rather than something your team can write.

If the underlying constraint is capacity rather than method, the same skills are available as embedded engineers. Teams commonly pair this work with hiring dedicated React Native developers in India or Flutter developers in India, so the people fixing what testing finds sit in the same working day as the people finding it.

Three Situations We Get Called Into

These are patterns rather than named clients, and they are described because they repeat. If one of them sounds like your app, the diagnosis is probably close.

The app that is fine on the team's phones

A product team on recent flagship devices, on fast wifi, ships confidently. Store reviews say the app is slow and crashes. Crash reporting shows a scatter of issues with no single dominant cause, and nobody can reproduce anything. What the analytics show, once someone actually looks, is that the largest single device model in the install base is a mid-range Android nobody on the team has ever held, with a fraction of the RAM and a much slower flash storage.

The work is unglamorous. Get three of those devices. Run the primary flows on them. Measure cold start, scroll performance and memory. Almost every time, the findings are the same family: images decoded at full resolution for a small view, a database read on the main thread that is imperceptible on fast storage and produces an ANR on slow storage, and a startup path doing work that could be deferred. None of it is exotic. All of it was invisible because the test devices were too good.

The upload feature that works until it does not

A field application where users capture photos or documents and upload them. It tests perfectly. In production, records arrive incomplete, some uploads never arrive at all, and users insist they submitted them. Support cannot reproduce it because support is in an office.

The reproduction needs a shaped network and an interruption script rather than a device. Throttle to a slow connection, start an upload, and background the app halfway. Kill the process during the transfer. Switch from wifi to cellular mid-upload. Fill the device storage and try to write the capture. In our experience the underlying cause is usually one of three: the upload is tied to a foreground activity and dies with it, there is no resumable or chunked transfer so an interrupted upload restarts from zero and eventually gives up, or the local queue is held in memory and lost on process death. The fix is architectural and cheap to specify once the failure is reproducible on demand.

The release that goes out and cannot be diagnosed

A version ships, the crash rate climbs, and the reports are unreadable because the mapping file for that build was never uploaded. The team spends a day reconstructing which commit produced the binary, another day rebuilding it to try to match the symbols, and by then the rollout has reached most of the user base.

The fix here is process rather than testing. Symbol upload becomes an automated build step. A deliberate test crash from the release candidate is verified as symbolicated before the build is promoted. Halt criteria are written down before the rollout begins, with a named person able to act on them during the first hours of the rollout. This is straightforward work and it is only ever done after it has hurt once. Where the release path itself is the weak link, it usually needs CI/CD pipeline work alongside the testing engagement.

What the Engagement Actually Looks Like

Sequencing matters more than the list. Building an elaborate automation suite on top of an app whose device matrix is guesswork produces confident tests of the wrong thing. So the order below is the order we work in, and the early weeks are deliberately about finding out rather than building.

First: the assessment

We start with your data rather than your app. Device and OS distribution from the stores and your analytics. Crash and ANR history by version, device and OS. Store reviews read properly, because users describe bugs badly but they do describe them. Your current test suite, if there is one, with an honest read on what it covers and how much of it is trusted. Your release process, including who presses the button and what happens when it goes wrong.

What comes out is a written picture: the device matrix with the reasoning attached, the highest-risk flows ranked, the gap between what is tested and what breaks, and a recommendation about what to do first. Occasionally that recommendation is that automation is not the priority, and that the release process or the crash reporting setup should be fixed before anyone writes a test. We would rather say that in week one than bill for the wrong work.

Then: the critical path

The flows that must never break get covered first, in whichever tool gives a trustworthy result fastest. Usually that means a smoke suite that runs on every build across a small device set, covering launch, authentication, the core transaction and anything that touches money. This exists to make the release decision faster, so it is judged on whether people believe it, not on how many cases it has.

Then: breadth and depth

The regression suite grows across the device matrix, and the specialist passes get scheduled rather than improvised: interruptions, network conditions, permissions across OS versions, battery and memory soaks, upgrade paths from previous versions, and the pre-submission checklist. Some of these run on every build, some on release candidates, and some on a schedule, and deciding which is which is part of the work. Running everything on every build is how a suite becomes something people skip.

Then: it belongs to your team

The handover set is specific: the device matrix with its reasoning, the test suite with documentation on how to run and extend it, the pre-submission checklist, the rollout halt criteria, the crash triage runbook, and a written record of the bugs found and what caused them. If your engineers cannot run and extend this without us, the engagement has not finished, whatever the pass rate says.

How Do You Run This From India When Our Users Are in Chicago?

This deserves a straight answer rather than a slogan, because the timezone question is the one that actually decides whether an offshore engagement works.

The overlap window, honestly

A standard working day in India runs roughly 09:30 to 18:30 IST. Against the UK that gives about four to five hours of genuine live overlap in the afternoon here and the morning there, which is comfortable. Against US Eastern time it gives almost nothing: our day is ending as yours begins. Against US Pacific it is worse. Against Australia and New Zealand the overlap is at the other end, with their afternoon meeting our morning.

Anybody who tells you this is solved by enthusiasm is selling something. It is solved by choosing a shift pattern deliberately and paying the coordination cost knowingly, and the pattern that works is a decision we make with you before the engagement starts rather than a promise made on a web page. What we will not do is claim round-the-clock coverage as though it were free. Shifted hours are a real cost to the people working them and they need to be planned, not assumed.

Why testing suits the gap better than most work

There is a genuine structural advantage here, and it is specific to this discipline. Testing is largely asynchronous by nature: a build goes in, a matrix run happens, results come out. If you push a release candidate at the end of your day, a full device-matrix pass can run overnight in your timezone and a written result with videos, logs and reproduction steps is waiting when you start. That is a working day you did not have to spend waiting.

The same applies to a rollout. The first hours after a staged release are the ones that matter most, and they usually fall in the middle of your night. Having people awake and watching crash rates during that window is one of the few places where distance is an asset rather than a cost.

The working practices that make it hold together

Written first, always. A bug report that requires a conversation to understand is a defective bug report. Ours carry the device model, the OS version, the build number, the exact steps, the expected and actual result, a screen recording, and the relevant log extract. If a developer twelve hours away cannot act on it without asking a question, it goes back.

A short daily written handover at the end of our day covering what was run, what was found, what is blocked and what runs next. A live call in the overlap window at a cadence you choose, weekly or a few times a week, used for judgement calls rather than status reading. Everything tracked in your tools, in your board, on your terms, because a separate tracker on our side is how two versions of the truth start.

One more practice worth naming: anything genuinely blocking gets raised at the start of our day, not the end. A blocker discovered at 10:00 IST and written up immediately reaches a US team's morning. The same blocker held until our evening costs a full day.

Communication and who you talk to

English capability is assessed in the interview through work, not through a certificate: reading a vague requirement and asking the right question about it, writing a bug report a stranger can act on, and explaining a technical trade-off clearly. The people doing the work are the people you talk to. We do not put an account manager between you and the engineers, because the detail that matters in testing does not survive being relayed.

Code, IP and access

Ownership of the code, the tests and everything produced sits with you, and the specific terms covering IP assignment, confidentiality and data handling are set in the agreement before work starts rather than described on a marketing page. Access follows the same principle: named individuals, your identity provider, least privilege, and revocation when someone rolls off. If your compliance regime imposes device or network requirements, those are agreed up front because they change how the team works and sometimes what is possible. Where personal data is involved, your own counsel should confirm what your regulatory position requires of a processor outside your jurisdiction. We can describe what we do; we cannot give you a legal opinion about it.

Risks, Blockers and What Usually Goes Wrong

Most engagements that go badly do so for reasons that were visible in week one. These are the ones we watch for, and the honest answer is that some of them are your side of the fence rather than ours.

No usable test environment

By far the most common blocker. There is no staging backend, or there is one but its data is stale, or accounts get reset nightly and every test breaks by morning, or the payment provider has no sandbox so nobody can test a purchase. Testing against production is not an option and testing against a broken staging environment produces failures that are about the environment rather than the app. This gets raised in the assessment and it usually needs your backend team, which means it needs scheduling early rather than discovering in week four.

An app that is hard to automate

Some apps resist testing for structural reasons. No accessibility identifiers anywhere. Views built dynamically with no stable way to address them. Business logic tangled into view controllers so nothing can be tested below the UI. A custom rendering layer that automation tools cannot see. In those cases automation is slower and more expensive than it should be, and the honest recommendation is sometimes to do a limited automation set plus a strong manual pass while the app is refactored, rather than pretending a brittle suite is coverage.

Devices that are hard to get

If your users are on a device only sold in one market, or your app depends on hardware such as a specific payment terminal, a Bluetooth peripheral or an industrial scanner, the device has to be sourced and that takes time and sometimes shipping. This is a planning problem, not a technical one, but it derails schedules when it surfaces late. It gets identified in the assessment along with who is providing what.

Nobody fixing what testing finds

A testing engagement produces a queue of defects. If the development team has no capacity to work that queue, the queue grows, the reports stop being read, and everyone concludes testing was not worth it. The failure was not testing, it was that finding and fixing were not resourced together. We flag this early because it is the most predictable way for good work to be wasted, and it is why testing capacity and development capacity should be planned as one number.

OS releases arriving on someone else's schedule

Both platforms ship a major OS version every year, with betas months ahead. Every year some apps break, and the ones that break in public are the ones that never ran against the beta. Testing against betas is unglamorous, occasionally produces bugs in the OS rather than your app, and is the only way to avoid a bad week in September. It belongs on the calendar as recurring work rather than as a reaction.

Timezone as a real cost

The overlap is what it is. If your team's working style depends on a quick synchronous conversation to unblock everything, distributed work will cost you more than it saves until that changes. Teams that write things down get most of the benefit and very little of the friction. Teams that do not, do not. That is worth being honest about before an engagement rather than three months into one.

Engagement Models

Three shapes cover almost every request. The commercial specifics, term, notice, scope changes and everything else are agreed in the contract before work begins, and this section is only about which shape fits which problem.

Dedicated testing team

Engineers embedded in your team, in your standups, on your board, working your sprints. This is the right shape when mobile releases are continuous, when testing needs to keep pace with development indefinitely, and when the value comes from people who know your app deeply rather than from a fixed deliverable. It is also the shape that suits building and then owning an automation suite over time, because the suite needs an owner.

Scoped project

A defined piece of work with a defined end: assess the current state and produce a device matrix and test strategy, build an automation suite for the critical path, run a full pre-launch pass on a new app, or bring an app up to standard before a major release. Suits a specific problem with a specific finish line, and it suits organisations that need to see the work before committing to a longer arrangement.

Ongoing release retainer

A regular cadence rather than a headcount: a full device-matrix regression run per release, an OS beta pass each year when the betas land, the pre-submission checklist before each submission, and monitoring during staged rollouts. This fits teams with their own developers who do not have the devices, the time or the specialist attention to do the release-testing work properly, and who want it to happen reliably rather than when someone remembers.

Which one to pick usually falls out of one question: is your problem a backlog or a rhythm? A backlog wants a project. A rhythm wants a team or a retainer.

Where This Sits Alongside Our Other Work

Mobile testing rarely arrives on its own. Where the app itself needs building or rebuilding rather than testing, that is mobile app development, and the two are often scoped together with testing running a sprint behind the build. Where the problem is broader than mobile, covering web, services and process, our QA and testing services cover the wider discipline and this page is the mobile-specific part of it.

If the real constraint is that your platform teams are short of people, the same skills are available as embedded engineers rather than as a testing engagement. That usually means hiring dedicated iOS developers in India or Android developers in India for native apps, and the React Native or Flutter equivalents for cross-platform teams. Testing finds problems; somebody still has to fix them, and pairing the two in the same working day removes a lot of waiting.

Where the release mechanism itself is the weak point, with builds that are hard to reproduce, symbols that go missing and a submission process that lives in one person's head, that is CI/CD pipeline work and it is worth doing first. A test suite that cannot run automatically on every build will not be run.

Frequently Asked Questions About Mobile App Testing in India

Do we really need real devices, or is a cloud device farm enough?

A cloud farm covers most of it and is the right default for breadth. What it handles badly is anything physical or sensor-driven: battery drain over hours, thermal throttling, camera and biometric flows, Bluetooth peripherals, NFC, and the behaviour of a genuinely old handset with a tired battery. Cloud devices are also usually wiped between sessions, which hides upgrade and long-lived-state bugs. We use a farm for the wide matrix and keep a small set of physical handsets for the cases where the farm cannot tell the truth.

How many devices should be in our test matrix?

Fewer than most vendor lists suggest, and chosen from your own analytics rather than a market share chart. We normally land on eight to fourteen configurations: enough to cover your top device families by session count, the oldest OS version you still support, the newest beta, one low-RAM budget Android, one small screen, one tablet or foldable if your data shows any, and one deliberately awkward device. The matrix gets reviewed every release because your user base moves.

Should we use Appium or the native frameworks?

It depends on who maintains the tests. Espresso and XCUITest run in-process with the app, so they are faster and far less flaky, but they are two separate suites in two languages owned by the platform teams. Appium gives you one suite across both platforms and can drive real devices in a farm, at the cost of speed and a synchronisation model that has to be handled properly. Maestro is worth considering for smoke and critical-path flows because it absorbs timing noise by design.

How do you test a flaky network without a flaky network?

By shaping it deliberately instead of hoping for it. Network Link Conditioner on iOS and macOS, the emulator network profiles and adb on Android, and a proxy such as mitmproxy or Charles in front of the app to inject latency, drop responses mid-body, return malformed payloads and hold a connection open until it times out. The interesting tests are not slow-network tests. They are what the app does when a request half succeeds and the user backgrounds the app during the retry.

Our app keeps getting rejected by App Store review. Can testing fix that?

A large share of rejections are mechanical and catchable before submission: a crash on launch on the reviewer's device, a broken or missing demo account, features that only work behind a login the reviewer cannot reach, permission prompts with no purpose string, sign-in options that do not meet Apple's requirements, and network code that fails on an IPv6-only connection. We build those into a pre-submission checklist and run it against a clean device. Rejections that are about business model or content are not a testing problem.

What does testing look like for a React Native or Flutter app?

The failure modes move rather than disappear. Flutter draws its own widgets onto a canvas, so an external automation tool sees very little until the semantics tree is enabled, and the integration_test package with Patrol for native dialogs is usually the sane route. React Native needs testID applied consistently and a synchronisation strategy for the JavaScript thread, which is what Detox exists for. Both still need native testing for permissions, deep links, background behaviour and store submission.

How do you catch battery drain before users complain about it?

With a repeatable soak rather than a stopwatch. On Android we reset battery stats, run a scripted session on a physical device off charge, then read the result through Battery Historian to see wakelocks, alarms, wakeups and network radio time attributed to the app. On iOS the Energy Log and the Instruments energy template do the equivalent. The usual culprits are a location subscription that survives backgrounding, a polling interval that ignores Doze, and analytics firing one request per event.

How does a mobile testing team in India fit around our release schedule?

A 09:30 to 18:30 IST day gives a UK team roughly four to five hours of live overlap and a US East Coast team almost none, so we agree the working pattern and the overlap window with you before the engagement starts. Practically, the day is arranged so a build submitted by your evening has a full device-matrix run and a written result waiting when you start. Anything genuinely blocking is raised in writing at the start of our day rather than held for a call.

Tell Us Which Device Your App Breaks On

Send us your platform, your top three device models by session count, and the bug you cannot reproduce. We will come back with where we would look first, what the device matrix should be, and whether testing is even the right thing to spend money on yet.

Start the Conversation