Internal, closed and open testing tracks
Kavita should not be the first person outside your desk to run Mandi Bhav. Google Play gives you testing tracks: the same app, delivered through the real Play Store, to a list of people you choose, before anyone else can find it. For a new personal developer account they are not optional: Google will not let the app reach production until a closed test has run with enough testers for long enough. This lesson covers the tracks, that requirement, the version numbers every upload needs, and the staged rollout that limits the damage when a release goes wrong.
As in lesson 3, the Play Console details come from Google's Play Console Help as it reads in November 2026; the console is not something this course signs up for. The version-number rules, and what Android does when they are broken, were run for real on the emulator.
The tracks
A track is a channel for releases. You upload a bundle, create a release on a track, and everyone the track reaches gets that version through the Play Store, with updates, exactly as they would from production. From the Help pages, under Test and release > Testing:
| Track | Who | How many | Notes |
|---|---|---|---|
| Internal testing | Testers you list by email | Up to 100 per app | Available within minutes; may skip the usual review; paid apps are free for internal testers |
| Closed testing | Email lists or Google Groups you choose | Lists of up to 2,000 testers each; a Google Group has no limit | You can run several closed tracks at once; the one the 12-tester requirement is about |
| Open testing | Anyone who opts in | Unlimited, or a cap of at least 1,000 | Findable on Play, from the store listing |
| Production | Everyone in the countries you choose | Can be released to a percentage first (below) |
Testers join with an opt-in link the console gives you for the track. Internal and closed tests are not searchable on Play, so you send the link yourself; it only works once the app's status is published. A tester who has opted in to the internal track gets internal releases only, until they opt out. And tester feedback goes to you privately: it does not touch the public rating.
What each is for, for Mandi Bhav:
- Internal: you, Priya and Arjun in the team, every build worth trying on a real phone from the real store. Minutes, not hours, so it is also where you check that the bundle Play built and signed (lesson 2) installs and runs.
- Closed: farmers and traders you know, through a Google Group, for weeks. Real use, real phones, real signal in a mandi yard.
- Open: when you want strangers' phones and opinions before production. Optional for many apps.
- Production: everyone, preferably in stages.
The closed-testing requirement for new personal accounts
Google's Help page states it in one line: personal developer accounts created after 13 November 2023 must run a closed test with "a minimum of 12 testers who have been opted in continuously for at least 14 days" before they can apply for production access. Organization accounts do not have this requirement.
Read the words carefully, because each has caught people out:
- 12 testers, opted in to the closed track. Not 12 installs from a link someone forwarded.
- Continuously. A tester who opts out on day 10 and back in on day 11 starts their 14 days again. Recruit more than 12, say 15 to 20, because some will leave.
- For the preceding 14 days when you apply. The count is checked at the moment you apply, from the Dashboard with Apply for production.
The application asks about the test itself: how hard it was to recruit testers, whether they used all the features, how their use differed from what you expect in production, what feedback they gave and how you collected it; then about the app (its audience, what makes it worth installing, an estimated first-year install range) and what you changed because of the test and how you decided it was ready. Google is asking whether you actually tested. Two weeks of 15 farmers opening Mandi Bhav every morning, a short form or a WhatsApp group for feedback, and a list of the three things you fixed because of them, answer that honestly. Twelve friends who installed it once do not.
The test is also where lesson 3's reports start working for you. Every upload gets a pre-launch report, and Android vitals counts the testers' crashes and ANRs, because they installed from Play. A crash on day 3 is a crash you fix on day 4, readable because the mapping was in the bundle.
versionCode: the number that must only go up
Every upload to Play needs a new versionCode, in app/build.gradle.kts:
defaultConfig {
applicationId = "com.riztechacademy.mandibhav"
versionCode = 1
versionName = "0.1"
}
versionCode |
versionName |
|
|---|---|---|
| Type | A positive whole number | Any text |
| Who sees it | Android and Play, never the user | The user, on the listing and in Settings |
| Rule | Each release higher than the last; never reused | Anything you like; "1.2", "1.2 (Diwali)" |
| Maximum | 2,100,000,000 on Google Play | None that matters |
Android uses versionCode to refuse downgrades, and Play refuses an upload whose versionCode this app has used before, whichever track it went to. The usual scheme is the simplest: start at 1 and add 1 for every upload, internal test builds included, because an internal build that took number 7 has used 7 for ever.
The trap: going backwards
Android enforces this on the phone, too. A scratch copy of Mandi Bhav, with its own applicationIdSuffix, was built twice, as versionCode = 3 and then versionCode = 2, both signed with the same upload key (lesson 2), and installed in that order:
adb install -r app-release-v3.apk
adb install -r app-release-v2.apk
Performing Streamed Install
Success
Performing Streamed Install
adb: failed to install app-release-v2.apk: Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 2 is older than current 3]
The phone keeps version 3 (dumpsys package still said versionCode=3). The same thing, in Play's form, is an upload that the console refuses. It happens in practice when a hotfix is built from an old branch whose build.gradle.kts still has the old number, or when two people bump it on two branches and both pick 8.
The trap: a clever number that does not fit
Some teams derive versionCode from the date and time, so it can never go backwards: yyMMddHHmm. For 9 November 2026 at 3.30 pm that is 2611091530. Put it in the scratch copy:
versionCode = 2611091530
e: file:///…/app/build.gradle.kts:59:21: Assignment type mismatch: actual type is 'Long', but 'Int?' was expected.
versionCode is a Kotlin Int, whose largest value is 2,147,483,647. A literal bigger than that is a Long, and the build file does not compile. Google Play's own ceiling is lower still, 2,100,000,000. A date scheme has to fit under that: yyMMddNN (a two-digit build number per day, 26110901) fits for decades; anything with minutes in it does not. Counting up from 1 has none of these problems.
Staged rollouts, and stopping one
A production release does not have to reach everyone at once. In the release, set a rollout percentage: Play offers the update to that share of your users, and you raise it later with Manage rollout > Update rollout. The Help page says it applies to updates (not an app's first release) on production, open and closed tracks, and that the percentage never rises by itself.
A sensible plan for a Mandi Bhav update: 5% for two days, watching Android vitals for that version, then 20%, 50% and 100%.
If vitals shows the new version crashing, Manage rollout > Halt rollout. What halting does and does not do matters:
- No more users get the new version.
- Users who already have it keep it. There is no button that takes it back off their phones.
- You cannot replace it with the previous build either: its
versionCodeis lower, and Android refuses downgrades, as above.
So the way back is forwards. Build the fix, or the previous code with a new, higher versionCode, and roll that out; the users stuck on the broken version get it as an ordinary update. This is why the staged rollout exists: when a release is bad, it is bad for 5% of farmers for a day, not for all of them for a week.
Mandi Bhav's path to production
| Step | Track | Who | How long |
|---|---|---|---|
| Every build worth trying | Internal | The team | Minutes |
| Recruit 15 to 20 testers into a Google Group | Farmers, traders, family | Before the closed test | |
| Closed test | Closed | The group | At least 14 days, continuous |
| Fix what the test found, re-release to the closed track | Closed | The group | As needed; each upload a new versionCode |
| Apply for production | You | When 12 or more have been opted in for the last 14 days | |
| First production release | Production | Everyone | One release (staged rollouts are for updates) |
| Every update after that | Production, staged | 5%, 20%, 50%, 100% | Days, with vitals open |
Check your work
Your closed test started with 14 testers. Two opted out on day 12 and back in on day 13. Can you apply on day 15? Only the 12 who stayed opted in for every one of the last 14 days count, so you meet the minimum exactly, with no margin. Had a third tester left for a day, you could not apply until 14 days after enough testers had been in without a break.
What did the emulator say when version 2 was installed over version 3? INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 2 is older than current 3; Android kept version 3.
Why does versionCode = 2611091530 not build? It is larger than Kotlin's Int allows (2,147,483,647), so the literal is a Long and the assignment does not compile; and Play's own maximum, 2,100,000,000, is lower still.
You halt a staged rollout at 20%. What happens to the farmers who already updated? They keep the broken version until you release a newer one with a higher versionCode.
Which track would you use to check that the bundle Play signs actually installs? Internal: releases reach testers within minutes.
Practice
- Decide Mandi Bhav's version scheme and write it in the README: what
versionCodethe first internal build gets, how it changes per upload, and howversionNamerelates to it. - Reproduce the downgrade on the emulator in a scratch copy with its own
applicationIdSuffix: buildversionCode = 3, install it, buildversionCode = 2, install it, and read the error. Uninstall the scratch app afterwards. - Write the recruitment message for Mandi Bhav's closed test in the words you would send to a farmer in a WhatsApp group: what the app is, what you want them to do for two weeks, and how to join with the opt-in link.
- Write the feedback form you would give testers: no more than five questions, at least one about the feature they used least.
- Harder: plan the release of a Mandi Bhav update that changes the database schema (module 8). Which tracks, which percentages, what you watch in vitals at each step, and what you do if the migration crashes on 3% of phones, given that you cannot roll back.
Official documentation
- Set up an open, closed, or internal test — The tracks, their limits, tester lists, opt-in links and how quickly each updates.
- App testing requirements for new personal developer accounts — The 12-tester rule and every question in the production access application.
- Release app updates with staged rollouts — Starting, raising, halting and resuming a rollout.
- Version your app —
versionCodeandversionName, and Play's maximum.
Next: Play policy, data safety, and staying published.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.
About the internship