Why sign-up emails are awkward to test on Android
- The emulator’s inbox is the wrong place to look. Signing in to Gmail on every emulator image takes time, and a shared account mixes codes from every tester and every run.
- Links have to open the app. A verify or magic link is usually an
httpsApp Link. If verification fails, Android 12 and later open it in Chrome without asking, and the flow quietly finishes in the browser. - Local links point somewhere else. On the emulator,
localhostis the emulator, not your computer. - Single-use links break if anything opens them first. Proxy Mail QA never opens or prefetches a link, so it still works when your test opens it.
Test it by hand on the emulator
- Create an address in Proxy Mail QA, with a persona and a generated password if your form needs them.
- Sign up in the emulator with it. The emulator shares your computer’s clipboard, so copy and paste works both ways.
- Get the code from the address’s row with Copy, and paste it into the app.
- Or open the link in the app:
adb shell "am start -W -a android.intent.action.VIEW -d '<link>'". For amyapp://link the inbox shows this command next to it, ready to copy. - If Chrome opens instead, check verification with
adb shell pm get-app-links com.example.appand retry it withadb shell pm verify-app-links --re-verify com.example.app.
Automate it with Espresso
Add the Kotlin helper from the Espresso guide to androidTest, pass the API key as an instrumentation argument, and the test reads the code itself:
val qa = ProxyMailQA()
val account = qa.createAddress()
onView(withHint("Email")).perform(typeText(account.address), closeSoftKeyboard())
val submitted = Instant.now()
onView(withText("Sign up")).perform(click())
val code = qa.waitForCode(account.address, submitted)
onView(withHint("Code")).perform(typeText(code))
In a Compose test, use composeTestRule.onNodeWithText("Email").performTextInput(account.address) and so on. To open a link, start an ACTION_VIEW intent with it (the guide has the two lines). If the email has both a web link and a myapp:// one, waitForLink(…, app = true) returns the app’s.
The same helper checks the email itself (checkEmail with your App Link domain, and countEmails for duplicates), signs in as a saved test account, and adds each test to the pull request’s report: see the Espresso guide.
What to check in your Kotlin code
- The email field is an email field.
android:inputType="textEmailAddress"for views, orKeyboardOptions(keyboardType = KeyboardType.Email)in Compose, so the keyboard doesn’t capitalise or “correct” the address. - App Links are verified for every build you test. The intent filter needs
android:autoVerify="true", andhttps://your-domain/.well-known/assetlinks.jsonmust list the SHA-256 fingerprint of the certificate that signed the build: your debug key for local builds, Google’s app signing key for builds from Google Play. - The link works from a cold start and a warm one. A link that launches the app arrives in
onCreateasintent.data. If the activity is already running withsingleToporsingleTask, it arrives inonNewIntentinstead, and is easy to miss. With Navigation Compose, anavDeepLinkon the destination handles both. - The link is still single-use. Open it twice: the second time should explain that it was used.
- Links don’t point at your computer. Use your staging address in the emails, not
localhostor10.0.2.2. See links that point at localhost.
Using Firebase email-link sign-in? Its App Links use your Firebase Hosting domain: see Firebase email-link sign-in on mobile.