All articles

How QA Can Prevent Bugs Before Testing Begins

A developer can build exactly what the ticket says, tick every acceptance criterion, pass code review, and still ship the wrong thing. The code was fine. The ticket was wrong. The bug was already sitting in the requirement before anyone wrote a line of code, which makes the requirement the first thing worth testing.

Most of the expensive bugs I have caught were caught in refinement, by asking a plain question about a sentence in a ticket while the answer was still cheap to change. No build, no browser, no automation running. Just a story, a design, and a handful of awkward questions asked at the right moment.

A lot of bugs never start life as code problems. They start as a vague requirement, a missing edge case, a risky assumption, a design with no error state, or a decision nobody questioned early enough. By the time those reach a test environment they are already expensive: the developer has built them, the team has reviewed them, and now you get to write the bug report that sends everyone back around the loop.

QA is risk thinking, not the gate at the end that says yes or no. A good share of that risk work never touches a running build, and it is some of the highest-value testing you will do all sprint.

A bug found early is a bug that costs almost nothing

The reason early QA pays off is not a vibe, it is one of the foundational principles of testing. The ISTQB Foundation Level syllabus lists “Early testing saves time and money” as one of its seven principles.

The reasoning is simple: a defect removed early will not go on to cause further defects in the work that gets built on top of it. The same misunderstanding gets dramatically more expensive depending on how far it travels before someone catches it:

Where you catch itWhat it takes to fix
Still a sentence in a ticketChange a sentence
Already written into codeChange the code and re-test it
Live in productionChange code, re-test, redeploy, handle the incident, talk to upset customers, and clean up the secondary problems it caused

The same idea has been called “shift left” for years, but I prefer the plain version:

The cheapest bug to fix is the one that never got built.

So most of the work is getting your input in before the build, not after it.

Here is how I think about where that input lands across the life of a story.

Cost to fix the same bug, by where you catch itSchematic. Direction only, not to scale.morelessRefinementDesign reviewDevelopmentTestingProductionThe further right you catch it, the more it costs. QA can push that catch point left.

The exact multipliers people love to quote here are shaky, so I will not throw a number at you, and the bars above are deliberately schematic to make that point: they show the direction, not a real ratio.

The direction, though, is not in dispute. The earlier you catch it, the less it costs, and the only way to catch it before development is to be in the conversation before development.

Bugs often start as misunderstandings, not mistakes

This is why “it matches the ticket” is never the bar you are aiming for. A ticket is a short summary of a decision, and the summary can be wrong, or half-made, while it reads perfectly clearly. The ticket itself can be the bug.

Take a requirement that looks completely harmless:

Users should be able to cancel their subscription.

One sentence, and it hides a dozen decisions nobody has made yet:

  • Can a user cancel during a free trial?
  • Does cancellation take effect immediately, or at the end of the billing period?
  • What happens to their existing data?
  • Do they get a confirmation email?
  • Can they undo a cancellation?
  • What if their payment is already past due?
  • What does the support team see on their side?
  • Which analytics event should fire?
  • How does this behave on mobile?
  • What copy appears on the confirmation screen?

If those questions get asked after the feature is built, the team is into rework. If you ask them during refinement, the team has a real shot at building the right thing the first time.

None of these questions require a running build. They require a tester in the room who is in the habit of poking at the gap between what was written and what was meant.

Review requirements for testability

A testable requirement is specific enough that the team can agree on whether it passed. If you cannot picture the test, the requirement is not done yet.

Watch for sentences that sound reasonable but cannot be verified:

The dashboard should load quickly.

Quickly for whom, with how much data, on which browsers? Compare that to:

The dashboard should show its first useful content within 2 seconds
for users with fewer than 100 projects, on supported browsers.

Or this familiar one:

Users should get a helpful error.

Against:

When a payment fails because the card was declined, the user sees a message
explaining the payment did not complete, and can try another payment method
without losing the contents of their cart.

The second version of each is something you can build, demo, and check. The first is a coin flip.

When I read acceptance criteria, I keep an eye out for the words that quietly hide all the disagreement:

  • fast
  • easy
  • user-friendly
  • properly
  • correctly
  • seamless
  • robust
  • handled

None of these are banned. They just are not finished. Each one is an invitation to ask “what would I have to see to call this correct?” and write the answer down before anyone starts coding.

Use concrete examples to surface the assumptions

The fastest way I know to expose a hidden assumption is to stop describing a rule in the abstract and write a specific example of it instead. Abstractions let everyone nod along while privately picturing something different. Examples force the disagreement into the open where you can deal with it.

Rule: a coupon can only be used once per customer.

Example 1:
Given a customer has never used SAVE10
When they apply SAVE10 at checkout
Then the discount is applied

Example 2:
Given a customer already used SAVE10 on a previous order
When they apply SAVE10 again
Then the discount is rejected
And the message reads "This code has already been used"

This style of conversation has a name and a shape. There is a lightweight workshop called Example Mapping: you put the story on one card, the rules on cards beneath it, a concrete example under each rule, and every question nobody can answer right now on its own pile.

A story with three rules, a couple of examples each, and no open questions is usually ready. A story buried in question cards is not, and you have just found that out in twenty-five minutes instead of mid-sprint.

Run it before iteration planning and you tend to see fewer stories bounced back later, because the disagreements got settled before anyone wrote code.

You do not need index cards or a formal ceremony to get the benefit. You need the instinct: when a rule is fuzzy, write the example.

Ask what happens when things go wrong

Teams design the happy path first because it is the part everyone can picture. It is the part where the API responds, the network holds, the data is present, and the user does exactly what the mockup assumed.

Your job is to ask about everything else.

For every feature, I run through the same short list of failure questions:

  • What if the API call fails or times out?
  • What if the user loses their connection halfway through?
  • What if the data is missing or empty?
  • What if permission is denied?
  • What if the user refreshes mid-action?
  • What if they click the button twice?
  • What if another user changes the same record at the same time?
  • What if a third-party service is down?

Failure states are not decorations bolted on at the end. They are part of the product. A checkout that handles a declined card gracefully is a better product than one that only works when the card goes through, and the difference gets designed in long before it gets tested.

This kind of “what could possibly go wrong?” thinking is just as useful pointed at a requirement as it is pointed at a running application.

Behind that failure list is a broader gut-check I bring to every feature, long before there is anything to click.

The questions I ask in refinement, before a line of code is written:

  • Is this requirement testable, or is it still an adjective like “fast” or “easy”?
  • What data does this feature quietly depend on, and what happens when that data is missing or wrong?
  • What are the unhappy paths nobody has written down yet?
  • What would make us uncomfortable shipping this, while it is still cheap to change?
  • And before anyone builds it: how will we know it actually worked?

Review the design before development starts

Design review is not only about visual polish. You can find genuine functional gaps in a mockup, and a gap caught in Figma is a gap that never becomes a defect.

When I look at a design, I am scanning for the states the happy-path mockup quietly skipped:

  • a missing empty state
  • error messages that are unclear or absent
  • no loading state
  • no disabled state for actions in progress
  • confusing navigation or no way back
  • a layout that has clearly only been drawn for a wide screen
  • a missing confirmation step before something destructive
  • colour contrast that will not pass WCAG
  • form labels that do not say what they want
  • no recovery path after a failure

A design might show a beautiful dashboard packed with data, and the questions that matter are the ones it does not answer:

  • What does a brand-new user with no data see?
  • What if one chart fails to load while the rest succeed?
  • What does someone with limited permissions see?
  • What happens when the screen is narrow?
  • What does the user see when the data is stale?

Those questions cost a five-minute conversation now. The same gaps cost a redesign, a re-build, and a re-test if you find them after the feature is “done.”

Make testability an engineering conversation, not an afterthought

Some features are painful to test purely because nobody designed them to be testable. Testability is a property you can ask for: how much can you control the system’s inputs and state, and how much can you observe what it actually did?

If both of those are low, testing is slow and unreliable no matter how good your tests are. The good news is that controllability and observability are cheap to build in while the feature is being written and expensive to bolt on afterwards.

So I ask for them up front:

  • stable selectors or accessible labels to target elements
  • API endpoints to set up and tear down test data
  • feature flags to turn behaviour on and off
  • test accounts
  • seeded data
  • logs for the events that matter
  • clear, distinct error codes
  • a sandbox mode for third-party integrations
  • a reliable way to reset state

Here is a concrete one. If you need to test subscription renewal, waiting out a real billing cycle is not a plan. The team needs a test mode, a way to move the clock, or an API helper that triggers the renewal on demand.

Asking for that is not QA being difficult. It is testability work, and it is far cheaper to agree on while the feature is still being built than to retrofit once it ships.

A refinement checklist you can actually use

I keep a short checklist for backlog refinement so the same gaps get caught every time instead of depending on whether I happen to think of them. Drop this straight into your refinement notes or your story template:

## QA refinement checklist

- Is the user problem clear?
- Are the acceptance criteria specific and testable?
- Are both the happy path and the failure paths covered?
- Are roles and permissions defined?
- Is test data available?
- Do we need analytics events or logs?
- Does this change existing behaviour?
- Does this need mobile, browser, or device coverage?
- Does this need an accessibility review?
- Does this touch payments, privacy, or customer trust?
- What here should be automated?
- What here needs exploratory testing?

The checklist is not there to slow anyone down or to turn refinement into a checkbox exercise. It is there because finding these gaps after development is far more annoying for everyone than finding them in a ten-minute conversation.

The instinct behind it is simple: decide early what kind of testing each piece of work actually needs, instead of treating every story the same and discovering the gaps at the end.

If you want help turning that into a deliberate plan, my test strategy builder walks you through the same decisions for a whole project.

A checklist with no owner is a checklist nobody runs, so pin down who carries each part and what tells you it is actually done:

  • Owner: QA. Run the checklist against every story before it is pulled in. Done when: no story enters the sprint with an untested adjective (“fast”, “easy”) still standing in for a requirement.
  • Owner: QA. Turn each open question into a concrete example or a written acceptance criterion. Done when: every rule the team disagreed on has an example under it and no open question is left unanswered.
  • Owner: Dev. Confirm the story is buildable as written and flag hidden dependencies. Done when: the developer can describe how they would build and demo each criterion.
  • Owner: PM. Resolve the product decisions the questions surface. Done when: the unhappy paths and edge cases have an agreed answer, not a “we will decide later”.

Sharpen acceptance criteria before the story is pulled in

Before a story enters development, you can review its acceptance criteria for the things that quietly cause bugs later:

  • missing negative cases
  • an unclear expected result
  • roles or permissions that were never mentioned
  • hidden dependencies on other features
  • assumptions about the data
  • assumptions about mobile or browser support
  • wording that simply cannot be verified

The payoff shows up fast when you turn a one-liner into criteria a developer and a tester can both build against. Compare the original:

Admin can invite users.

with the version that actually says what “done” means:

Given an admin is on the team settings page
When they invite a user with a valid email
Then the invited user receives an invitation email
And the pending invite appears in the team member list

Given an admin invites an email that already belongs to the team
When they submit the invite
Then the app shows "This user is already on the team"
And no duplicate invite is created

Given a non-admin opens team settings
Then the invite-user action is not available to them

That second version is better input for the developer and better input for the tests. The negative case, the duplicate case, and the permission case are all on the table before a line of code exists, which means they are decisions instead of defects.

When the time comes to automate, name those tests for the behaviour they verify, like verify that an admin can invite a user with a valid email, so a failure tells you exactly what broke. If you want to go deeper on turning criteria into a real suite, test automation that pays off picks up where this leaves off.

Let production bugs sharpen your early questions

Every defect that escapes to production is trying to teach you something about your refinement, and it is worth listening. When one gets through, I run a quick, blameless retro on the question rather than the person:

  • Was the requirement unclear?
  • Was test data missing?
  • Was the scenario never considered?
  • Was there no automation coverage?
  • Did the test environment differ from production?
  • Did we miss a device, browser, or permission?
  • Did we ignore a warning sign?

Then I feed the answer back into the refinement checklist, the acceptance-criteria patterns, or the regression coverage so the same gap cannot open twice. The point is never to assign blame. The point is to stop paying for the same mistake more than once.

Closing that loop properly is the heart of a quality strategy that actually works, and it is what turns a string of escaped bugs into a system that quietly gets better.

My take

You do not have to wait for a finished build to create value. When you review requirements, ask the uncomfortable questions, check designs for the states the mockup skipped, think through failure paths, and push for testability while it is still cheap, a meaningful share of bugs simply never get built.

That is not invisible work, even though it can feel that way because there is no bug report to point at afterwards. It is quality work at its highest leverage. Finding bugs matters and always will. Preventing them matters even more.

This is the front end of building quality in; for the back end, once a bug does slip through and you have to carry the fix all the way to a shipped build, I walk through the release workflow from bug report to beta build.

If you only change one thing after reading this, get yourself invited to refinement and bring the checklist. Most of the rest follows from being in the room.

Found it useful? Share it.
Julia Pottinger

Written by

Julia Pottinger

Hi, I'm Julia. I've been in QA for over a decade. I spend my days testing software and my own time building apps and games, and I write here to share what I learn, the practical, honest lessons you can actually use.

Comments 0

Share your thoughts, ask questions, or add to the conversation.

Be kind and constructive. Stay on topic. No spam or self-promotion.
Loading comments…