How Do I Test and Debug Apps Built With Vibe Coding?
A vibe coded app nearly always demos well. You describe what you want, the model writes it, the screen fills up, and by the end of the afternoon there's something you can tap through and show people. The trouble arrives a week later, when a real person taps the same buttons in a slightly different order and the whole thing falls over. Testing vibe coded apps is a different job from testing software a team wrote by hand, because nobody in the room can tell you why any given line is there.
Code written by a model is confident everywhere, including all the places it's quietly wrong.
We design and build products for lifestyle brands, and more of the briefs reaching us now arrive with a working prototype attached. Somebody has already built the thing. What they want to know is whether it's safe to put in front of paying customers, and that question has a real answer.
Getting to it takes a bit of structure. You need to know what the app is supposed to do, read enough of the code to form a picture of how it hangs together, test the parts that carry money and personal data hardest, and then watch real people use it. This article walks through how we do that, what breaks most often, and the point at which patching stops being worth it.
Why AI-Generated Code Fails Differently
Hand-written code fails where the person writing it was tired, rushed or out of their depth. You can almost predict the spots. The bugs gather around dates and time zones, around the integration somebody finished at six on a Friday, around the feature that changed shape twice during the build. Generated code spreads its mistakes evenly, because the model was equally sure of itself on every line.
The failures have a recognisable character. A function that handles the happy path and swallows the error case without telling anyone. Two copies of the same calculation in different files, one of which got updated last week. An API call shaped the way that service's documentation looked three years ago. Validation on the front end with nothing behind it on the server.
Two-thirds of developers surveyed by Stack Overflow, 2025 named AI code that's almost right as their single biggest frustration, and "almost right" is the phrase to hold on to. Almost right passes a demo. Almost right survives the first five taps and then stops.
So testing vibe coded apps starts from a different assumption. Any part of the surface can be broken while looking completely normal, which means you sample across all of it rather than heading straight for the bits you'd expect to be hard.
Reading Code You Did Not Write
You can't test what you don't understand, and understanding is most of the work. Developers spend somewhere between 58% and 70% of their time on code comprehension according to studies cited by Minelli et al. and Scalabrino et al., and that's for code with an author sitting nearby. With generated code there's nobody to ask.
A reading order before testing vibe coded apps
Don't start with the file list. Start with one piece of data and follow it all the way through. Take an email address from the sign-up screen and trace where it goes, what validates it, what stores it, what sends it on to a third party. Then do the same with a payment, a saved item, an uploaded photo.
You can ask the model to explain each file, and that's a reasonable first pass. Treat the explanation as a guess that needs checking, because it was generated the same way the code was. Read the function it describes and see whether the two agree.
Write down what you learn as you go. A short document naming every entry point, every stored piece of data and every outside service the app talks to is worth more than any amount of re-reading, and it becomes the map you test against.
Design that understands your users
We build app experiences around real user behaviour, not assumptions. Research, psychology-driven design and technical specs that turn users into loyal advocates.
Deciding What 'Working' Actually Means Before You Test
A property developer came to us wanting a concierge app for their high-rise buildings. They arrived with a budget already set and a fairly fixed idea of the feature list, and what they really wanted from us was a sign-off rather than a scoping exercise. We talked them into a proper discovery phase with focus groups and user workshops first.
What we found was that the product needed to be far simpler than they'd pictured. Plenty of the planned features never got built. We focused instead on creating a human connection with the product rather than replacing the systems already humming away in the building, and they got a better product for less money.
Testing tells you the code runs, and only a definition of done tells you it does the right thing.
That's the piece people skip with a generated build, because the build already exists and feels finished. Before you write a single test, write down what the app has to do to be worth shipping.
- The three or four journeys that must work every time
- What the user sees when each of them fails
- The data that must never be wrong, lost or shown to the wrong person
- The slowest acceptable response for anything involving money
Unit and Integration Tests When the AI Writes Those Too
Ask a model to write tests for its own code and you get tests that pass. They're generated from the same understanding that produced the bug, so they agree with it. A test asserting that a discount function returns 12.50 is worthless if the function and the test both got the rounding rule wrong.
The fix is to split the work. You write the assertion, in plain terms, from your definition of done. A basket of three items at £18 with a 15% code applied comes to £45.90. Let the model write the scaffolding, the mocks and the setup around it, because that part is tedious and the model is good at tedious.
Write your expected values by hand on paper before you look at what the code returns. Once you've seen the output, it's almost impossible to disagree with it.
Then aim the tests at the joins rather than the interiors. A function that formats a date needs one test. The path from checkout button to payment confirmation to order record needs ten, because that's where generated code tends to make assumptions about what the previous step handed it.
Keep the suite small enough that people run it. Forty meaningful tests that run in thirty seconds beat four hundred that nobody waits for.
Testing Against Real Hardware, APIs and Third-Party Services
Generated code is fluent about services it has never actually called. It knows roughly what a payment provider's API looks like, roughly what a shipping service returns, roughly how an email platform handles a bounce. Roughly is fine until a real response arrives with a field missing.
So every outside connection gets tested against the real thing in a sandbox, not against a mock the model invented. Watch what happens when the token has expired, when the rate limit hits, when the webhook fires twice for the same order, when the response takes eleven seconds instead of two hundred milliseconds. Generated error handling is usually a catch block that logs nothing and carries on.
This is where storing your infrastructure as code in the repository pays for itself. Being able to spin up a fresh, identical environment in minutes means you can break things properly, with real keys and real integrations, and then throw the environment away. That consistency and speed is something a human deployment process can't match.
Hardware is harder again, because the device usually isn't ready when the software is. You test what you can reach and you build something to stand in for the rest, which is the subject of the next section.
Building Harnesses That Let You See Inside the System
On a baby monitor project we had app software ready well before the hardware existed in any usable form. So we built a prototype that rendered a small web server from the device itself. We could connect to it directly, see the internals of what was going on, confirm that a setting sent from the app had actually been applied, and drive the device from the app at the same time.
That gave us both ends of the conversation at once. Rather than guessing from the app's own screens whether a command had landed, we watched it land. When something went wrong we knew straight away which side of the link had dropped it.
The same thinking applies to a purely software build. Put a hidden debug screen in the app that shows the current state, the last ten network calls and whatever's sitting in local storage. Add a way to force the app into any state you like without playing through twenty minutes of setup first.
With generated code you're often working out what the system believes about itself. A harness that shows you its internals turns a long afternoon of guessing into ten minutes of looking.
Testing the Joins Between Features
On a dating app project built around verified profiles and keeping bots out, the client wanted to skip discovery on the messaging side and put everything into the onboarding process. We built a generic messaging feature because that's what the brief allowed, and it quietly contradicted the entire premise of the product. It permitted automated messages and fake messages, undoing all the verification work the onboarding had done so carefully.
The whole messaging section had to be rewritten. That came to roughly £15,000 of extra budget and about two months of extra work, and every individual piece of code involved had worked exactly as specified.
Each feature can be perfectly correct on its own and the product can still be broken between them.
Generated code makes this failure more likely, because each prompt produces a feature in isolation with no memory of the promises the rest of the app has made. The verification logic doesn't know the messaging logic exists.
So test the sequences, not the screens. Sign up, abandon halfway, come back on a different device, change your email, then try the thing the first session started. That's where the contradictions surface.
Security, Data Handling and Permissions Checks
Generated code tends to be generous. It asks for broader permissions than the feature needs, stores more than it uses, and puts checks in the places a user can see rather than the places a user can't reach. An admin screen hidden from the menu is still an endpoint anyone can call.
Work through the list your own reading produced. Every endpoint gets asked the same question, which is what happens if a logged-out stranger calls it, and then what happens if a logged-in stranger calls it with somebody else's record ID. Check where API keys live, because models are fond of putting them in the client bundle where anyone can read them.
Open the app's own network traffic in a proxy tool and read what it's actually sending. People are regularly surprised by the amount of data leaving their product.
Then apply a simple test to the data you're collecting. If you had to tell users exactly what you were doing with a piece of data and why, and they knew all of it, would they still hand it over? Where the honest answer is no, the collection is hiding its purpose and needs handling a different way. A generated analytics integration will happily hoover up far more than you'd ever defend out loud.
Performance, Load and Behaviour Under Stress
Performance problems are the single biggest reason apps get turned away at the door. Apple's own figures show performance violations, meaning apps that crash, freeze or fail to show their features, accounted for about 1.24 million of 1.93 million App Store rejections in 2024, more than every other category put together, according to the 2024 App Store Transparency Report.
Generated code is prone to a specific kind of slowness. It fetches inside loops, it reloads a full list to change one item, it keeps everything in memory because that was simplest to write. On a developer's new laptop with twelve test records, none of that shows up at all.
So test with volume that looks like real life. Two thousand products in the catalogue, four hundred messages in a thread, a user with six years of order history. Then test on a four-year-old mid-range Android handset on a patchy train connection, which is where a good share of your customers actually are.
Watch what the app does while it waits. Generated loading states are often missing entirely, so the screen just sits there and people tap the button again, which fires the request a second time.
Putting the Build in Front of Real Users
Automated testing confirms the app does what you told it to. Watching people use it tells you whether what you told it was right, and the two find completely different problems. Testing with just five users uncovers around 85% of usability problems according to Nielsen Norman Group, so the cost of finding out is low.
We run these sessions observationally. We ask somebody to carry out a particular action or take a particular route through the product, then we watch for hesitation. Are they pausing on a screen to work out what's being asked of them? Do they pick the right path, or set off down the wrong one with apparent confidence? Those pauses are cognitive overload showing itself, and generated interfaces produce a lot of them because the wording comes from a model with no feel for the brand or the moment.
One caution about anything involving money. In a session nobody is really paying, so they judge the checkout rationally and tell you it seems fine. Live analytics show something else, because a detail that looked harmless in a test room is enough to erode trust at the moment of actually parting with cash.
Instrumentation: Catching the Bugs Testing Missed
Between funding rounds on a product of ours, we made the call to put in much better tracking of user retention and to go out and ask people proactively how things were going. We'd been using investor confidence as a stand-in for product readiness, and we didn't properly understand the health of our own retention. That decision changed what we knew about the product within weeks.
Investor confidence is a poor substitute for knowing whether anyone comes back.
High-level funnel numbers won't get you there. The granular behaviour is what carries the signal, and it needs capturing deliberately rather than hoping an off-the-shelf integration covers it.
- Time spent on each screen, not just whether people reached it
- People entering a screen, leaving, and going back into it repeatedly
- Scrolling up and down through terms or explanatory copy
- Errors and crashes with enough context to reproduce them
Repeated re-entry and repeated scrolling signal either a lack of comprehension or a lack of trust. Alongside that, read your support requests for patterns. A common theme in what people ask usually unpicks back to one specific point of confusion in the app.
A Debugging Process for Code With No Author
Debugging vibe coding output goes wrong when people paste the error into the model and accept whatever comes back. That produces a second change you don't understand sitting on top of a first change you didn't understand, and two days later there are five of them.
Work through it in order instead.
- Reproduce it reliably, with the exact steps written down, before touching anything
- Find the boundary between where the data is still correct and where it isn't
- Read the code at that boundary yourself, slowly, including the parts that look obvious
- Form a specific theory about the cause and say it out loud
- Make one change, test it, and keep it only if the theory held
- Add a test that fails without the fix, so the bug can't quietly return
If a fix works and you can't explain why, revert it. An unexplained fix is a second bug waiting for a worse moment.
The model is genuinely useful inside this process. Ask it to explain what a function does, to list every caller of a method, to suggest three causes for a symptom. Treat its answers as leads to check rather than conclusions to act on.
Regression Control When Regeneration Rewrites Working Code
The particular pain of generated builds is that asking for a small change often returns a large one. You wanted the button moved and you got the whole component rewritten, with the edge case you fixed last Tuesday quietly removed.
Small commits are the defence. Commit after every working change, with a message that says what it does, so that any regeneration can be read as a diff and accepted line by line. Anything touching money, authentication or stored data gets read in full before it goes in, however tidy the rest of the change looks.
Keep a short file listing the fixes that were hard-won, with the reason each one exists. When regeneration deletes one, you'll recognise what's gone.
Put the tests from your definition of done into an automated run on every commit, and treat a red result as a stop rather than a nuisance. Lightweight tool-assisted review takes under 20% of the time of formal inspection and finds just as many bugs, according to a SmartBear study of Cisco Systems, so a quick human read of each diff is cheap insurance.
Keep environments reproducible too, so that a broken build can be swapped for a known-good one while you work out what happened.
Knowing When to Stop Patching and Rewrite
On a sports club app project we warned the client early that the budget was heading somewhere ugly, and that they were in real danger of running out of money before anything reached the store. The warning was noted and the feature requests kept coming. We got to the end of the project with the entire budget spent, nothing published, and the two parties going their separate ways.
Patching past the point of sense does the same thing on a smaller scale. Each fix is cheap, the total is enormous, and the product still isn't ready. The judgement is easier with a few signals in front of you.
| Signal | Keep patching | Rewrite the section |
|---|---|---|
| Bug frequency here | First or second time | Fourth time this month |
| Understanding | You can explain the flow | Nobody can explain it |
| Cost of a fix | An hour, contained | A day, and it breaks two other things |
| What it touches | Display only | Money, personal data, permissions |
| Test coverage | Tests exist and pass | Can't be tested as it stands |
Rewriting one section with a clear specification is usually days rather than weeks, and it's the cheaper option far earlier than people expect.
Conclusion
Vibe coding gets you to something real with remarkable speed, and that part is genuinely useful. What it doesn't give you is the understanding that normally comes free when a person writes the code, so that understanding has to be rebuilt deliberately through reading, testing and watching people use the thing.
None of it is exotic. Decide what working means before you test. Follow the data through the system until you can draw it. Aim the hardest testing at money, personal data and the joins between features, because that's where generated code contradicts itself. Build something that lets you see inside the system, the way the web server on the baby monitor device let us watch both ends of the conversation at once. Then instrument the live product properly, so the bugs your testing missed come back to you as evidence instead of as churn.
The products that go wrong are rarely the ones with a bug in them. They're the ones where nobody stopped to ask what the thing was supposed to do for the person using it, and a model can't answer that question for you.
If you've got a build that works in a demo and you're not sure it's ready for customers, we're happy to take a look at it with you. Let's talk about testing your vibe coded app.
Frequently Asked Questions
Hand-written code tends to break in predictable places, around rushed deadlines, tricky integrations or features that changed shape mid-build. Generated code spreads its mistakes evenly because the model was equally confident on every line, so any part of the app can be broken while looking completely normal. That means you sample across the whole surface rather than heading straight for the areas you would expect to be hard.
It looks like a function that handles the happy path and silently swallows the error case, or two copies of the same calculation sitting in different files with only one kept up to date. It also shows up as API calls shaped around documentation that is years out of date, or front-end validation with nothing backing it up on the server. All of these pass a demo and survive the first few taps before falling over.
Don't start with the file list. Pick one piece of data, say an email address from the sign-up screen, and follow it all the way through the app to see what validates it, what stores it and what passes it to a third party. Then repeat the exercise with a payment, a saved item and an uploaded photo.
You can, and it is a reasonable first pass for getting oriented. Treat the explanation as a guess that needs checking though, because it was generated the same way the code was. Read the function it describes and confirm that the two actually agree.
Anything that carries money or personal data. These are the areas where a quiet failure costs you real customers, real refunds or a real data problem, rather than just an awkward screen. Test them harder and earlier than the cosmetic parts of the app.
Studies suggest developers spend between 58% and 70% of their time on code comprehension, and that is for code with an author sitting nearby to answer questions. With generated code there is nobody to ask, so budget generously. Understanding is most of the work, not a preamble to it.
Yes. Keep a short document naming every entry point and every piece of stored data as you go, because that record becomes the map you test against. Without it you will re-learn the same things repeatedly and miss the parts of the app nobody has looked at.
Testing gets you a long way, but you also need to watch real people use the app. Problems surface when someone taps the same buttons in a slightly different order from the one you anticipated, which is exactly the scenario a demo never covers.
There comes a point where the volume of quiet errors and duplicated logic makes each fix riskier than the last, and a rebuild of the affected area becomes the cheaper option. If you cannot trace how data moves through a section without unpicking it entirely, that is a strong signal. The honest answer is usually clear once you have mapped the app properly.
Related Articles
What Are the Limitations of No-Code App Development?
Here's a fact that caught me off guard when I first saw it: no-code platforms now power over 65% of...
What Makes Your Mobile App Valuable to Potential Buyers
Two apps with the same monthly revenue can be worth very different amounts, and the difference...