Most of the apps we take over have no tests. Either there never were any, or there are a few written at the start of the project that stopped compiling long ago. As long as nobody changes the app, nobody notices. The problem starts with the first update: raising targetSdk, a new framework version or swapping the payments library changes things nobody meant to touch, and there’s no way to check that everything still works as before.
This article isn’t about reaching 80% code coverage. It’s about getting, at a reasonable cost, to a point where shipping a new version stops being a lottery.
Why tests matter in an app you only maintain
Maintaining an app is largely about changes you don’t decide on: store requirements, new OS versions, deprecated SDKs. Any of them can break something far from the place it touches. Updating a networking library can break sign-in, and raising targetSdk can break notifications.
Without tests there are two options. Either someone clicks through the app by hand before every release, which is expensive and unreliable, or your users test it for you, which then shows up in your store ratings.
Don’t start with code coverage
A coverage percentage is a convenient number, but in an old app it points in the wrong direction. The easiest things to cover are simple helper classes that rarely break, and the hardest are the things that break most often: screens, integrations and flows that cross several modules.
A better starting point is the question: what costs the most when it stops working? Usually that’s:
- sign-up and sign-in, including with Google and Apple,
- payments and subscriptions,
- the main flow users install the app for, such as placing an order or making a booking,
- push notifications,
- the first launch after installing and after updating from the previous version.
That’s a handful of flows, not hundreds. That’s where you start.
Step 1: a pre-release checklist
The cheapest start involves no code. It’s a written list of steps someone runs on a real phone before every release:
Flow What to check Install clean install, first launch, system permissions Update installing the new version over the previous one, whether the user stays signed in and keeps their data Sign-in email, Google, Apple, password reset Main flow from start to goal, e.g. to a placed order Payment a purchase in the test environment, a cancellation, returning from the banking app Push receiving a notification and opening the app from itWriting such a list takes a few hours, and running it on Android and iOS takes from a quarter of an hour to an hour before each release. The row skipped most often is updating from the previous version, even though every existing user goes through exactly that.
Step 2: free tools you already have
- The Google Play pre-launch report. When you upload a build to a testing track, Google automatically runs it on physical devices, crawls through it and reports crashes, performance and accessibility issues. It’s free and switches on by itself, yet hardly anyone looks at it. If the app requires sign-in, you can provide test account credentials in the report settings.
- Internal testing on Google Play and TestFlight. A build released to the internal testing track on Google Play and to TestFlight in App Store Connect can be checked before launch by the team and a few trusted people. For a wider group, TestFlight lets you add up to 10,000 external testers.
- Crash monitoring. Crashlytics or Sentry won’t replace tests, but with a gradual rollout they show a problem before it reaches everyone. We cover this in our article on mobile app crash monitoring.
Step 3: automated tests for key flows
Once the checklist is stable, its most important items are worth automating with UI tests that launch the app on an emulator and walk through the flow the way a user would. The tools depend on the technology:
- Maestro — scenarios in a simple YAML format, works with native apps, Flutter and React Native. For older apps without tests it’s often the quickest start, because it needs no changes to the app’s code.
- Espresso and XCUITest — the native Android and iOS tools, giving more control but requiring more work.
integration_testin Flutter and Detox in React Native.
A few practical rules:
- Five to ten scenarios are enough to start with. Each additional one is a maintenance cost whenever the UI changes.
- Tests need a test environment. A separate backend, or at least test accounts and payments in sandbox mode. Tests on production end in real orders and real notifications.
- A flaky test is worse than no test. If a test passes one time and fails the next, the team soon stops paying attention to the result. Such tests have to be fixed or removed straight away.
Step 4: tests with every change
You don’t have to write unit tests for the whole app at once. One rule is enough: before you change a module, capture in tests how it works now. These characterisation tests pin down the current behaviour, quirks included, and show you if a change breaks it by accident.
They pay off most in logic that doesn’t depend on the screen: calculating prices and discounts, validating forms, handling dates and time zones, mapping API responses. That way tests accumulate where the app actually changes. We go into this in more detail in our article on extending an existing app.
What to test on
You don’t need fifty phones. A few configurations chosen from your user data are enough:
- the oldest supported OS version — this is where new libraries break most often,
- the newest OS version — this is where behaviour changes surface, as we describe in our article on why an app stops working on the new Android or iOS,
- a small screen and a large font size — this is where layouts fall apart,
- a popular low-end Android phone — this is where performance and memory issues show.
Emulators in CI, the pre-launch report and cloud device farms such as Firebase Test Lab cover the rest.
Tests in the release process
Tests you have to remember to run stop being run. Ideally, automated tests run on every change to the repository, and a release is only possible when they pass. How to set this up and what it costs is described in our article on release automation with CI/CD and fastlane.
What it costs
Roughly, for a medium-sized app on both platforms:
Item One-off cost Ongoing cost Pre-release checklist 2–4 hours 0.5–1 hour per release Pre-launch report, internal testing, TestFlight set up as part of a release PLN 0 Test environment and test accounts 8–24 hours depends on the infrastructure UI tests for 5–10 key flows in CI 40–80 hours fixes when the UI changes Unit tests with changes included in each change —At an example rate of PLN 180 net per hour (real market rates vary), the full set comes to roughly PLN 9k–20k as a one-off. For comparison: a single release with broken payments that reaches all users for a few days usually costs more, and leaves a mark on your ratings too.
What not to do
- Don’t pause work for a few months “to write the tests first”. Tests should grow alongside maintenance, not instead of it.
- Don’t make a coverage threshold the goal. The team will start writing tests that raise the number rather than the safety of releases.
- Don’t test against the production backend or on real customer accounts.
- Don’t ignore flaky tests. A single flaky test teaches the team that a red result can be ignored.
Summary
An old app without tests doesn’t need a big testing project. Start with a pre-release checklist, use the stores’ free tools, automate a few of the most important flows and add tests with every change. After a few months, updates stop being risky and the cost of each release drops noticeably.
Tests for key flows and a pre-release checklist are standard in the apps we look after as part of mobile app maintenance. If you want to know what your app’s release process looks like and where to start, order a free app analysis — you get the results in writing.