Writing

AI Flattened the Testing Pyramid. Here Are Six Tests to Add.

AI made every layer of the testing pyramid about equally cheap. These are the six tests I added above my unit tests in two days, with $0 spent on CI servers or device farms, each with the prompt that built it.

16 min read

  • Android
  • Testing
  • Claude Code
  • CI/CD
On the left, workers on scaffolding haul stone blocks up a tall, crumbling testing pyramid under a stormy sky. On the right, one person with a tablet watches glowing test blocks drop into a single flat row. The headline reads: AI Flattened the Testing Pyramid. Here Are Six Tests to Add.

Pick the test you're missing Six kinds of test, each with a copy-and-paste prompt.

The testing pyramid was really a price list. The tests at the top were expensive to write, to keep working and to run, so I skipped them. AI made every price about the same.

I build two Android apps alone: Driftwell, a sleep app that plays all night with the screen off, and RuckPace, a GPS pace coach for rucking. Between them they had thousands of unit tests and not one that drew a screen, listened to TalkBack, or checked that playback pauses for a phone call and picks back up after.

DriftwellSleep affirmations that play all night. In production.
RuckPaceA GPS pace coach for rucking. In internal testing.

One weekend in October Claude built the rest. It took two days and costs $0 a month to run: every test runs on a computer I already owned, and the phones I don’t own come from Firebase Test Lab’s free daily quota, so there’s no CI bill and no device-farm bill. The one cost left is time, so the slow tests run after the merge, where nobody waits on them.

Pick the test you’re missing

Each section is one kind of test: what it finds, the prompt, and how it works in my apps. Add only the ones you’re missing.

1. Catch broken layouts on every phone size Every screen checked on a small phone, in dark mode, at large text and in landscape, on every pull request.
2. Catch the accessibility bugs most apps ship with A build that fails when a button has no spoken label or text is too faint to read.
3. Prove your app survives real-life interruptions A phone call, headphones coming out and deep power saving, tested in 11 minutes after every merge.
4. Turn your real-world use into tests Your own real-world sessions become permanent tests you can run at a desk.
5. Find crashes on phones you don't own Every release crawled on five phones before it reaches production, plus a new set every night, for $0.
6. A QA checklist for what can't be automated A checklist on your phone for what only a person can judge, with the results saved next to the plan.

Whichever you add, finish with the two prompts in Before you trust any of them. I ran everything with Superpowers, so each prompt is a goal and a definition of done, not a recipe.

1. Catch broken layouts on every phone size

Screenshot tests: small phone, dark mode, large text, landscape

FindsWords cut in half, clipped numbers, buttons squeezed off the screen.
RunsEvery pull request, no emulator. 50 seconds for Driftwell's 43 images, 3.5 minutes for RuckPace's 145.
NeedsNothing new. It runs like a unit test.

The prompt

Add a screenshot test tier with Robolectric and Roborazzi. Render every main screen on five setups: phone light, phone dark, a 360dp small phone, font scale 2.0, and landscape. Compare each render with an approved image, and fail the pull request on any difference until I approve it. Also fail on words broken mid-word, text touching text, clipped text, and controls squeezed below 24dp. Show me every finding before you fix or suppress anything.

How it works

Screens are drawn without an emulator, using Robolectric and Roborazzi, on five setups: a regular phone in light and dark, a small phone, double font size, and landscape. The first run at double font size was the worst:

BugsRuckPace active screen at double font size with four bugs circled: the pace number clipped, mi12:21 run together, Behind broken onto two lines, and the Settings gear missing
RuckPace mid-ruck at double font size, Android's largest text setting, the first time the screenshot tests drew it. (1) The pace number is cut off under the gauge. (2) "mi" and "12:21" run together. (3) "Behind" breaks into "Behin / d". (4) The Settings gear is gone, squeezed to zero width.

I fixed each one and then made it a check, so a word broken in half, text touching text, or a clipped label fails the build on its own.

2. Catch the accessibility bugs most apps ship with

Automated accessibility checks on every screenshot

FindsWhat a screen reader gets wrong: buttons with no spoken label, and buttons that all sound the same. Faint text too.
RunsEvery pull request, inside the screenshot run. No extra time.
NeedsThe screenshot tests from section 1.

The prompt

Run Roborazzi's accessibility checks on every screenshot render, and add the screenshot tier first if there isn't one. Fail the build on missing labels, low contrast and duplicate spoken text. Before trusting any of it, write a guard test that renders an unlabeled button and unreadable text and proves the build goes red. Show me every finding before you fix or suppress anything.

How it works

Each screenshot also goes through Google’s accessibility checks, the same ones the Play Store’s pre-launch report runs. The biggest find was what TalkBack, Android’s screen reader, said out loud on Driftwell before and after the fixes:

BeforeAfter
"Move up" "Move down" "View messages", the same on every row"Move Abundance up" "Read Abundance"
"Delete", six times on one screen"Delete" plus the name of the set
"Play a sample", ten times"Play a sample of" plus the voice's name
"On, switch"The channel's name with its switch
"Selected", read as if it were a pictureAnnounced as the row's state

At first the checks passed everything. An older test library showed the checker a blank screen, so a button TalkBack couldn’t name still passed, and the build stayed green.

That’s why the prompt asks for a guard test: an unlabeled button and unreadable text the checks must reject. If they ever stop catching problems, that test fails first.

3. Prove your app survives real-life interruptions

Emulator tests: phone calls, headphones out, deep power saving

FindsPlayback that dies on a phone call, with headphones out, or in deep power saving.
RunsAfter every merge, never on a pull request. About 11 minutes, emulator boot included.
NeedsAn emulator on a machine you own.

The prompt

Swap in whatever your app has to survive in the background.

Add instrumented tests that run on an emulator the script starts itself, on a fixed console port. Test what my app must survive: a phone call (dialed through the emulator console), another app taking audio focus, headphones unplugged, and screen off in deep Doze. Pin ANDROID_SERIAL so nothing installs on a phone that happens to be plugged in. Run them in CI on merges to main only, never on pull requests. Use a throwaway probe to check any system output format before you parse it.

How it works

Driftwell’s job is to keep playing for hours while Android and other apps try to stop it. None of that shows up in a unit test, so eight tests run on an Android 16 emulator after every merge. Each one does to the app what a real night does:

What happens at nightWhat Driftwell has to do
A phone call comes inPause, then pick up where it left off
Navigation or another app talks over itLower its volume, then come back
Another app starts playing musicStop
Headphones come outPause, not play out loud at 2am
Screen off, phone in its deepest power savingKeep playing until the sleep timer stops it
Every row is an automated test. All eight run in about 11 minutes, for $0.

The phone call is my favorite. The emulator can fake an incoming call, but only from the computer it runs on. The test runs inside the emulator, so it reaches back out to the computer and dials itself.

4. Turn your real-world use into tests

Replay tests from recorded GPS or sensor data

FindsGPS bugs that only happen outside: dropouts under trees, jumps when the screen locks.
RunsEvery pull request, with the unit tests. Under a second.
NeedsOne real recording from your own phone.

Fake GPS only tests the cases you think of. A real walk brings the ones you don’t, like tree cover, a locked screen or a pace that wanders, and once it’s recorded, every later change has to pass it.

The prompt

For anything that depends on the world outside: GPS, sensors, Bluetooth.

Add a debug-logging switch in settings that records every raw GPS fix and every decision the app makes to a file the user can share. Write a replayer that feeds a log back through the same code the app runs. Then turn my next real recording into a permanent test, with the coordinates shifted so it can't show where I live, and prove the shift changes no distance, pace or alert.

How it works

RuckPace's Debug logging switch and the Debug logs row to share a saved ruckA real RuckPace walk replayed: raw GPS dots against the app's smoothed pace, rejected fixes, and the spoken alerts as they fire
Turn on debug logging, walk a ruck, share the log. Then replay it at the desk: grey dots are raw GPS, the green line is the pace the app trusted, and three fixes were too noisy to use.

I recorded the GPS on an ordinary walk to my mailbox. Now every change to the pace engine is replayed against that walk on every pull request, so a change that gets distance, pace or a spoken alert wrong fails before it ships.

These logs are real walks near my house, so every point is shifted 145 degrees east before it’s committed, and every distance, pace and alert comes out the same. The full story is in how I ship these apps.

5. Find crashes on phones you don’t own

Robo crawls on real devices in Firebase Test Lab, free

FindsCrashes on phones and Android versions you don't own.
RunsBefore every production release: 5 to 10 minutes, phones in parallel. Again nightly, where nobody waits.
NeedsA Firebase project on the free plan. No card.

I test on one or two phones. My users run hundreds of models, many with the maker’s own changes to Android, and a crash that only happens on one of them reaches them before it reaches me.

The prompt

Test Lab’s free plan has no card on file, so it can’t bill you.

Before my release script promotes a build to production, have it download the APK Google Play will serve and run a Firebase Test Lab Robo test on two real and three virtual phones. Ship only if every device passed: a timeout or a missing result counts as a failure. Stay on the free Spark plan with no billing account. Add a nightly job that spends the rest of the daily quota crawling the live build on a rotating set of phones and opens a GitHub issue with the stack trace when one crashes. Release at a 20% staged rollout, and refuse any commit that isn't merged to main with every CI check green.

How it works

Google Play’s pre-launch report is meant to do this for free. It never ran for me, even after two fresh uploads to closed testing. Google support later told me why: the report no longer runs on every upload, only when Google has spare capacity. It runs on Firebase Test Lab, so I went there directly. The free plan caps you at 5 runs a day on real phones and 10 on virtual ones, with no billing account, so there’s nothing to charge.

  • Before every production release, Robo, Test Lab’s crawler, works through the exact APK Play will serve on two real and three virtual phones. A crash, a timeout or a missing result stops the release.
  • Every night, the leftover quota crawls the live build on a different set of phones, and a crash becomes a GitHub issue with the stack trace.

Robo finds a crash on a screen, not a session that dies after an hour. The same prompt sets a 20% staged rollout, so Android vitals sees a slice’s crash rate first. With users this few, I still override it and release to everyone.

6. A QA checklist for what can’t be automated

Manual tests, written by Claude

FindsWhat only a person can judge: how TalkBack sounds, whether a real purchase goes through.
RunsWhenever a change needs a person, on your own phone. As long as the checklist takes.
NeedsYour time. This is the one layer that still costs it.

AI builds features faster than I can look at them, and each one comes with tests saying it works. A passing test proves the code does what it was told to do. It can’t tell me whether that’s what a user wants. The checklist is how I check the product: one page per change, so I keep up with what’s shipping instead of trusting a green build.

The prompt

For TalkBack, real purchases, and anything that needs a person’s eyes.

Write a manual QA page for this change as one HTML page I can use on my phone. Each case is a card with Do steps, Expect bullets, a Pass / Fail / Skip toggle and a notes field, with a running tally at the bottom. Put the Play build, commit and PR at the top, and save results on the page. For TalkBack, write exactly what it should read aloud. Only include what the automated tests can't prove, and say why at the top.

How it works

Manual testing took longer than anything else here, and TalkBack longest of all: you turn it on, close your eyes, and swipe through every screen listening. So any change that needs a person comes with a QA page Claude writes for someone holding a phone, and the page keeps the results.

A RuckPace QA page for the onboarding style picker, part way through a run, with Pass, Fail and Skip marks, notes, and a running tally
A real RuckPace QA page; the marks and notes are illustrative. The header pins the run to a Play build, a commit and a PR, so a failure is a version number rather than "the new one." The bar at the bottom keeps the tally.

It lists what the automated tests already cover, so I only test what they can’t, and it heads off false alarms: “Get Plus” always fails on the test build, so purchases get tested on the internal track.

Before you trust any of them

A test that checks nothing looks exactly like one that works, and Claude writes them faster than I can read them. So every test has to fail once before I trust it.

  1. Break itMake one realistic change to the app, like deleting the line that pauses when headphones come out.
  2. Watch it failRun the test. It has to go red, with the message you expected.
  3. Put it backUndo the change. Green again.
Five deliberate breaks on the emulator tests, five red tests.

The dangerous mistakes were the ones that passed. The first accessibility gate checked nothing, and a volume check would have said the sound never dropped while it visibly did. A red test also has to fail for the right reason: one that runs out of memory after 10 minutes proves nothing.

Ask for this after any batch of tests Claude writes:

For each test you just wrote, make one realistic change to the production code that the test should catch, run it, and show me the failure message. Then revert it. If a test stays green, say so: that test is the bug. A failure from a timeout or running out of memory doesn't count.

Then have a fresh session review the work. Mine found a safety check that didn’t check what its comment promised:

Review this branch as someone who didn't write it. Look for tests that would pass while checking nothing, comments that promise a check the code doesn't make, and anything that could touch my own phone, emulator or data. Rank what you find, and only flag what you can point to in the code.

A test nobody has watched fail is a guess, and when AI writes tests faster than anyone can read them, watching them fail is the review.

Where each test runs

The pyramid still holds for one thing AI can’t fix: waiting. A ten-minute test still takes ten minutes, so put that wait where it costs the least. This is my pipeline.

CI/CD pipeline
Runs on every change. One build moves through the stages in order, and each starts only if the one before it passed.
  1. Pull requestGitHub Actions, self-hosted runner$0 · ~10 min avg · you wait
  2. Merge to mainGitHub Actions, emulator on the same machine$0 · +11 min (Driftwell) · nobody waits
  3. Internal testingship-testers skill → fastlane android testersMy time
  4. Release gateship-production skill → fastlane android productionFree · 5–10 min
  5. Production
    • 20% staged rollout, then everyone
    • Android vitals watches crash rates
    Same lane: promotes the exact build testers hadFree
Nightly job
Runs on a schedule, whether or not anything changed. The release gate checks the same few phones so a release can't slip past them. This job spends the rest of the free daily quota on phones the gate never sees.
  1. Every night, on the live build
    • Crash crawl in Test Lab on 9 different phones each night, working through every device Test Lab has
    • Blocks nothing: a crash becomes a GitHub issue
    Scheduled GitHub Actions → fastlane android robo_rotation. A Claude Code startup hook lists open crashesFree · leftover daily quota
Each test links to its section. A pull request takes about 3 minutes for Driftwell and 16 for RuckPace, which draws 145 screenshots. Nothing ships until every check is green, including the slow ones after the merge. Amber stages are free layers I had never used.

The $0 also depends on where it runs. GitHub’s free minutes ran out two days into my month; a self-hosted runner on a machine I already had costs nothing.

Move this repo's CI to a self-hosted runner on this machine. Unit tests and lint run on every pull request. Keep the repo private so no fork can run code on my machine, and tell me anything the hosted runner provided that this one won't.

Two things most people reach for first aren’t on the chart: a matrix of emulators across five Android versions, and a paid device farm. I priced both, and they’re in the next section.

What’s still expensive

Two days and $0 didn’t buy everything. There are tests I still don’t run, and problems I haven’t fixed.

Tests I don’t run

  • A paid device farm. Test Lab past the free quota costs $5 an hour per real phone and $1 per virtual one; AWS Device Farm is $0.17 a device-minute. The free quota already covers a release gate and a nightly crawl.
  • Emulators on five Android versions. Free, but five times the 11-minute run. The nightly crawl covers other versions instead.
  • A real eight-hour night in CI. CI runs two minutes of deep power saving. The real night happens on my nightstand.

Where it’s still rough

  1. Google’s free crash report never ran. Play promises a pre-launch report from real phones. I uploaded to closed testing twice and never got one. Google support says it now runs only when they have capacity, and the Play Console doesn’t tell you. Test Lab does that job now.
  2. Crashes in Driftwell’s speech code are unreadable. That code comes from a library that strips out what a crash report needs, so a crash there shows up as a list of memory addresses. Play’s documented one-line fix does nothing for it.
  3. Some phone makers kill apps overnight. Some brands stop background apps far more aggressively than stock Android, and a sleep app that dies at 2am is exactly the review I’m afraid of. Nothing in this setup runs on those phones, so I don’t know whether Driftwell survives them.

What actually took the time

The code was the fast part. The time went into breaking every test on purpose and listening to TalkBack read every screen.

The testing pyramid taught me these tests were fragile and would take more time to debug than they’d ever save, so I skipped them. They weren’t. Claude wrote them, they found more than a dozen real bugs across both apps, and they’re staying.