Bootcamp
Writing testable software and testing in the real world
Photo by Andy Sanchez on Unsplash
Ask why software projects fail and most people reach for a technical answer. The honest answer is less comfortable: projects fail through a failure to set and manage realistic expectations, on both sides of the table. Inadequate understanding of the requirements, under-investment in testing, plain miscommunication. An app packed with ambitious features that struggles to ship is a riskier proposition than a simple one that works properly, and testing is where that risk gets managed or ignored.
The right mindset comes first: assume the worst, not the best. Users won’t set out to break your app, but they will break it, and your assumptions are what get you into trouble.
Testing is assessing the functionality of a software program, checking for errors and gaps, and asking whether the outcome of the application matches the desired expectations. It happens during development, not just after it, and everybody is involved: developers first, then QA, then clients and stakeholders.
The types of testing worth knowing
Functional testing checks the app against its business requirements. Black-box techniques capture the client’s view of the world: put an input in, check the output, ignore the machinery. Developers want to watch the process; clients genuinely don’t care.
Within functional testing sit four workhorses. Unit testing exercises the smallest pieces (a form submission, an API workflow returning what it should), and developers do it continuously as they build. Regression testing re-checks things that already worked after new changes ship, and it’s a notorious pain point in Bubble development; the defence is architecting the app in sections, so a change in one place can be made with confidence that it won’t ripple somewhere unrelated. Integration testing checks how modules work together, such as the handoff from onboarding into a dashboard that must welcome a first-time user without breaking on every later visit. Acceptance testing assesses the whole system against the requirements to define completion and sign-off.
Non-functional testing covers the quiet requirements: performance, security, accessibility, compatibility. Stress testing tries to break the app under worst-case conditions or a surge in activity; security testing hunts for data you didn’t mean to expose; performance testing measures speed and stability under a given workload. To the usual list of non-functional requirements (secure, reliable, maintainable, scalable) it’s worth adding one more: testable.
All of it runs on test cases: defined scenarios with inputs and expected outputs, recorded somewhere honest, whether a Google Sheet, a Trello or Asana board, or a proper project management tool for heavier programmes. As for automation: Bubble doesn’t offer the automated testing a code stack does, and services like Preflight that record click-through flows tend to respond poorly to app changes, so you rebuild scenarios endlessly. Know automated testing exists, then plan to test manually and well.
A testing routine for Bubble
Bubble rewards small, agile teams: a solo developer or two or three people covering development, architecture, UX and product. Our working philosophy is to deploy at 98 or 99 per cent confidence and be on hand to fix issues quickly, rather than running one-to-two-week test cycles after every change that squander Bubble’s speed. A few practices make that safe rather than reckless.
Watch performance from the first unit. If a form submission or a repeating group load isn’t snappy while you’re developing it alone, it will be miserable with concurrent users. Performance problems are cheapest at the moment of construction.
De-risk with proofs of concept. Identify the hardest parts of the app, then build a tiny separate Bubble app that demonstrates just that integration or UI challenge before committing to the real build. Often you can do this before even accepting the project.
Test with realistic data. Not five records when reality is fifty. Build workflows purely to generate and update data in bulk (even workflows the app itself doesn’t need), push fifty items through your API workflow, test the recursion. And where your logic forks, test every branch with the datasets that trigger it; one green path proves nothing about the other three. The logs and capacity tabs show how the app is coping, and under Bubble’s workload-unit pricing an inefficient bulk workflow isn’t just slow, it’s expensive.
Test security by looking, not hoping. Build a private testing page that loads your data types into repeating groups, then run as different user types and see what actually reaches the browser. The debugger will show you fields privacy rules have stripped.
Unit test like a saboteur. On a settings page with conditional forms: press cancel and check whether data is lost or inputs properly reset; jump between browser tabs; hammer the back and forward buttons and reload mid-flow; feed in invalid inputs and empty entries and try to save.
Integration test the awkward paths. Sign-up, onboarding and dashboard make three modules with many routes between them. What happens when someone abandons onboarding, not by clicking anything, just a phone call and a closed laptop? When they return, logged in but incomplete, do you prompt them back before the app becomes unusable? Payments deserve special paranoia: a typical Stripe action creates a customer, saves a card and processes the charge in one step, so one invalid input can leave you half-succeeded, with a payment but no order or vice versa. Decide in advance whether you process the order and chase payment, or push the charge to the back end and update the order when Stripe’s webhook confirms.
Run user acceptance testing against written criteria. A spec or at least a feature brief defines done; the core question is “have we solved the main problem?”, not “is it flawless?”. Fixed-price projects breed an endless find-a-bug, fix-it, find-another loop at the end. And learn to spot UX improvements disguised as bugs: “it doesn’t work the way I imagined” is a legitimate enhancement request, but it’s a new phase of work, not a defect, and clients need that framing early.
Writing testable software
Testing goes faster when the software is built to be tested. That’s a design discipline of its own.
Expose the underlying data. A total or a bar chart invites the question “how do I know that’s right?”. Pair it with a table of the raw records, or teach the client to read a database view, and validation becomes self-service.
Build bypasses for awkward dependencies. A payment gateway shouldn’t stand between you and testing everything downstream of it; a button that flags a user as paid simulates the state directly.
Give yourself a debug mode. Use Get data from page URL with a key of debug_mode, and put conditionals on hidden groups so they appear only when the URL carries debug_mode=true. Expose intermediate calculations that other logic depends on. Better still, break complex conditions into checkboxes: if an element shows only when three things are all true, bind each to its own checkbox, write the visibility expression as “checkbox A is checked and checkbox B is checked and checkbox C is checked”, and in debug mode you can see at a glance exactly which condition is failing. It’s more readable logic in ordinary times and X-ray vision when testing.
Let admins see what users see. Write expressions against Parent group's User instead of Current User, and the same screens can render any user’s perspective: an admin picks a user from a dropdown and walks the field-tech flow as them. Retrofitting this onto a hundred Current User expressions is agony; deciding it up front costs almost nothing.
Keep it simple. Complexity doesn’t add to your testing burden, it multiplies it: forked logic is two times two times two, not two plus two plus two, because every combination of paths needs a walk-through. If you want testing to be easy, stop multiplying.
Scaffold yourself with audit logs. Good developers aren’t perfect; they diagnose and fix fast, and logs are what make that possible. Log data changes (a database trigger comparing before and after), unhandled page errors, named events (sign-up, login, purchase, profile completed) coded with searchable descriptors, and navigation: browser details, IP and location, parameters, paths, session IDs. When the CEO of a big client “couldn’t get through the flow and the app’s broken”, you trace their session, correlate events with errors, notice they were on an iPhone, and discover the mobile-only button whose workflow was never given the right parameter. The problem described is rarely the actual problem; logs triangulate the real one. And ask bug reporters for a full screenshot, URL bar included.
Simulating real-world factors
The lab is not the world. These are the gotchas that surface only when reality is allowed in.
- Live differs from test. Integration URLs and OAuth redirect URIs often change between versions, so test the live version itself: put a real payment through, connect the real calendar. Use the
Website home URLexpression instead of hard-coding URLs, and expose the API call’s URL in-app where possible. - Concurrency creates race conditions. What happens when different users interact with the same data at the same time, such as one exporting while another creates records mid-export? If you’ve only ever tested alone, you’ve never tested this.
- Test every role. You’ve tested admin exhaustively because that’s who you are in the editor; the restricted team-member role, tested twice in passing, is where the surprises live.
- Time zones and time-based logic. Store each user’s time zone and offset in minutes on the User so you can flip perspectives while testing. For logic gated by schedules or elapsed time (a button that only shows on weekdays, values within thirty minutes of a date), build in overrides, even a field specifying the date you’re pretending it is.
- Browsers and devices. Each browser has subtle differences, and mobile testing gets skipped because the debugger is awkward on a phone; do it anyway, with device-simulation extensions helping to close the gap.
- Emails escaping the test environment. Testing from a database that contains real users can genuinely notify them. Guard every send-email action with the
isn't live versionexpression: in test, route to users flaggedtest user = yes(or an option set of addresses); in live, send normally. The safeguard matters most on the day you must copy live data back into test to reproduce a real incident.
Underpinning all of it: get fluent with the debugger, especially for evaluating dynamic expressions and what lists are returning; read the server logs, remembering that concurrent users’ entries arrive intermingled; and always review the database, because “is the data being created what I expected?” is the question that catches the most bugs.
Where testing sits in the project
Testing isn’t a phase; it’s threaded through every stage. During UX, UI and technical design, usability testing asks “is this actually useful?” (a confusing nav with sixteen undifferentiated options is a defect too), while senior developers de-risk the hard problems, sometimes by not building at all: teaching a client to administer content in the Bubble editor can beat four weeks of admin screens. During development, developers unit and integration test thoroughly; the biggest failure mode is rushing the happy path and letting “it’s buggy” become the project’s reputation. QA then documents test cases and compiles bug lists; user acceptance testing earns client sign-off and seeds the enhancement backlog; and at deployment, someone walks every main flow in live to confirm nothing failed to translate. The objectives never change: good software, cost- and time-efficient delivery, client confidence, and timelines you actually hit.
This skill in the AI era
AI has made writing code cheap and made this module more valuable, not less. When Claude Code generates a feature in minutes, the constraint shifts entirely to verification: test cases, realistic data, race conditions, logs. The same techniques transfer directly, except now the agent can write the seed-data scripts and the automated tests Bubble never could, if you know to demand them. That judgement, knowing what to test and what “done” means, is what we bring as a Claude Code development partner, and it’s a skill that moved across from Bubble to AI-assisted coding without losing a step.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
API calls as actions vs data (and the planning checklist)
When to use Bubble API calls as actions versus data sources, a one-to-many auth pattern, and the planning checklist that keeps integrations from failing silently.