QA Engineer Interview Questions · 2026

QA Engineer Interview Questions (2026): 25 Most Commonly Asked, With Answers

A QA engineer candidate at a Series B logistics startup got one technical question for 35 minutes straight during a January 2026 loop: "here's our checkout page, tell me everything you'd test before we ship it." No LeetCode, no Selenium syntax quiz, just a blank whiteboard and a live product screen. She walked through the functional cases first, then the edge cases, then what happens when the payment gateway times out mid-transaction. She got the offer. The two questions that followed in her debrief were about a SQL join and a defect she'd have logged as high severity, low priority.

QA Engineer interview questions at the mid-level look a lot more like that than like a trivia round. This page isn't a manual-testing-fundamentals primer and it isn't a Selenium tooling walkthrough, those live on their own pages. It's what an actual QA Engineer interview loop tests past entry-level: SDLC and STLC judgment, test strategy and case design, defect management, functional-versus-non-functional testing, enough API and SQL to hold your own, when automation is worth building, and how QA operates inside a sprint. The Bureau of Labor Statistics puts median wage for software quality assurance analysts and testers at $102,610 as of May 2024, with the broader developer and QA category projected to add roughly 129,200 openings a year through 2034, per the BLS Occupational Outlook Handbook.

One opinion, and it might be wrong: most QA Engineer interview questions still overweight the automation-tool checklist, can you name Selenium's wait strategies, do you know Cypress, relative to what actually predicts whether someone's good at the job, spotting the three cases nobody wrote down. Tooling clearly matters some. The PractiTest 2026 State of Testing Report found automation coverage is the second most-tracked QA metric industry-wide at 40.1% of teams, right behind test coverage at 56.4%. I just don't think tooling knowledge is the differentiator interviewers think it is at mid-level. A tester who can't design a test case well writes brittle automation no matter the framework.

2-4Rounds
Test Design+SQL+APICore Focus
Awareness levelAutomation
1-3 weeksPrep Time

SDLC and STLC: what a QA Engineer needs to know cold

Companies open with SDLC and STLC because it's the fastest way to tell whether a candidate has worked inside a real release process, or just memorized a flashcard deck.

Easy questions

15

SDLC, the Software Development Life Cycle, covers the entire process of building software: requirements, design, coding, testing, deployment, and maintenance. STLC, the Software Testing Life Cycle, is the subset of phases testing owns specifically: requirement analysis, test planning, test case design, environment setup, test execution, and closure. STLC runs inside SDLC, not alongside it as a separate track.

The follow-up that separates a fresher from a mid-level answer: name which STLC phase overlaps with which SDLC phase, and explain why planning starts during requirements, not after a build lands. Waiting for code to start planning is a common reason teams miss testing deadlines, not because testers are slow, but because nobody scoped the work early.

A test strategy is the high-level, usually project-wide document: testing types in scope, tools, environments, general approach. It rarely changes once approved. A test plan is feature- or release-specific: what's being tested this cycle, who's testing it, the schedule, entry and exit criteria, and the risks for this release.

A quick way interviewers catch a shaky answer: ask which document needs updating if a new release adds a payment feature. It's the test plan, since scope changed, not the strategy, since the overall approach hasn't.

New, the defect gets logged. Assigned, routed to a developer. Open, someone's working it. Fixed, the developer hands it back. Retest, QA verifies the fix against the original steps. Then Closed if it holds, or Reopened if it doesn't, and the cycle repeats. Some teams add a Deferred or Rejected state for won't-fix defects, or for duplicates.

Interviewers push on the Reopened path specifically: how many times is acceptable before it escalates, and who decides a fix failing verification repeatedly needs a different developer or a design conversation instead of another attempt.

Functional testing checks whether the system does what it's supposed to, does clicking "place order" place the order. Non-functional testing checks how well it does it, how fast, how secure, how much load it survives before it falls over.

For checkout, three non-functional types outrank the rest: performance, does the payment step respond fast enough that a user doesn't abandon the cart; security, is card data handled correctly and never logged in plaintext; and reliability under load, does checkout stay stable during a flash sale. Usability and accessibility matter too, they rank slightly behind those three here.

A GET request is idempotent with no side effects, run it a hundred times and nothing on the server changes. A POST that creates a resource has side effects, every run creates new data, so the suite needs a cleanup step or isolated test data, or you end up with a database full of orphaned test orders after a few hundred CI runs.

The practical follow-up: how do you keep a POST-heavy suite from polluting shared environments. The usual answer is tearing down created resources after each test, or running against an environment that resets between suites.

An INNER JOIN returns only rows with a match in both tables. A LEFT JOIN returns every row from the left table, with NULLs filled in for any right-table columns where no match exists.

QA leans on this for data integrity checks. Finding orphaned records is a LEFT JOIN's whole job, an INNER JOIN would silently exclude exactly the broken rows you're trying to find.

sql
SELECT o.order_id, o.customer_id
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL;

Smoke testing is a shallow, wide check run right after a new build lands, before any deeper testing starts. Does the app launch, does login work, do the core pages load without throwing errors. It's usually automated, takes a few minutes, and its only job is deciding whether the build is stable enough to bother testing further.

Sanity testing is narrower and deeper, and it happens after a specific fix or small change, not after every build. If a developer just patched the checkout flow, a sanity check means poking at checkout and the areas right next to it to confirm the fix worked and nothing obviously adjacent broke. It's usually manual and unscripted. Teams mix the two up constantly, but the trigger is different: smoke gates a new build, sanity gates a specific fix before it goes into full regression.

Regression testing means re-running previously passing tests to confirm that a change, a bug fix, a new feature, a refactor, didn't quietly break something that used to work. You run it after any change that touches shared logic, before a release goes out, and often on a schedule against the main branch, like nightly.

The practical challenge is scope. A full regression suite can take hours, so most teams tag a smaller critical-path subset that runs on every pull request, and save the full suite for pre-release or nightly runs. Skipping it to hit a deadline is how old, already-fixed bugs quietly come back, which is literally what the word regression means here.

Manual testing is a person executing steps and using judgment on the result. Automated testing is a script doing the same steps and comparing against an expected value, over and over, without a human watching each run. Neither replaces the other completely.

The rule of thumb I use: if a test will run many times without changing, like regression suites, smoke checks, or API contract validation, automate it, because the upfront cost pays for itself after a handful of runs. If the feature is brand new, still shifting shape, or the thing you're checking is subjective (does this feel confusing, does this layout look right), do it manually first. Automating a UI that changes daily just means you spend all your time rewriting broken scripts instead of testing.

A test scenario is a one-line description of something to verify, like 'user can reset their password.' It's a heading, not instructions. A test case is the concrete, executable version of that: specific input data, exact steps, and the exact expected result, like 'enter a valid registered email, click reset, confirm the email arrives within two minutes, confirm the reset link expires after one hour.'

One scenario usually produces several test cases once you account for valid input, invalid input, an unregistered email, an expired link, and a link that's already been used. Scenarios help you scope what to cover in planning; test cases are what actually gets executed and reported on.

Verification asks 'are we building the product right,' and it happens without running the software: design reviews, requirement walkthroughs, static code analysis, checking that a spec was actually implemented as written. Validation asks 'are we building the right product,' and it happens by actually executing the software against real user needs, usually through testing.

You can pass verification and still fail validation. A feature can be built exactly to spec, reviewed and signed off, and still be the wrong thing because the spec itself didn't match what users actually needed. That gap is why QA sits close to both requirements review and hands-on testing, not just one or the other.

Exploratory testing is simultaneous learning, test design, and execution, without a pre-written script telling you the next step. You give yourself a charter, something like 'find problems in the refund flow for partial returns,' set a time box, maybe 45 minutes, and just work the feature, following anything odd you notice.

Scripted tests only catch what you thought to write down in advance. Exploratory testing catches the stuff nobody anticipated: a weird interaction between two features, an error message that leaks a stack trace, a field that accepts input it shouldn't. It's not just 'clicking around' though, good exploratory testing still produces notes and a session report so the findings are repeatable and reportable, not just a vague 'seemed fine.'

A stub is a fake dependency that returns a canned answer so the thing you're actually testing can run in isolation. If you're testing a function that calls a pricing service, a stub just returns a fixed price object, it doesn't care whether or how it was called.

A mock goes a step further and verifies the interaction itself: did the code call this method, with these arguments, exactly once. Stubs support state-based testing, checking what came out. Mocks support behavior-based testing, checking what was called. Overusing mocks is a real trap, because tests start asserting on implementation details, and then a harmless internal refactor breaks a dozen tests that never should have cared how the code got its answer.

Hardcoded data like test@test.com or a fixed user ID of 1 works fine until two things happen at once: tests run in parallel, or the same suite runs twice against a shared environment. Now two test runs are racing to create, update, or delete the exact same row, and you get intermittent failures that have nothing to do with the actual feature being broken.

The fix is generating unique data per test run, usually by suffixing with a timestamp or a UUID, so test-1689213-user@test.com never collides with anything else. Pair that with setup that creates exactly what the test needs and teardown that cleans it up, rather than relying on data some other test happened to leave behind. Shared fixtures across a whole suite are convenient until one test mutates the fixture and every test after it inherits the mess.

Black-box testing means you test based on inputs and expected outputs without looking at the code, driven by requirements and behavior. Most functional, UI, and API testing that a QA engineer does day to day is black-box.

White-box testing means you have full visibility into the code and design tests around its internal structure, like unit tests that specifically target a branch of an if-statement, or code coverage analysis to find untested paths. Gray-box sits in between: partial knowledge of the internals. A common example for a QA engineer is knowing the database schema and checking the actual row state after hitting an API endpoint, even though you didn't write the endpoint's code. Most senior QA work ends up gray-box in practice, because you rarely test something you understand zero about.

Medium questions

25

Testing should start the moment requirements exist, not when a build lands in QA. The V-model makes this explicit, pairing each development phase with a matching testing phase on the other arm: requirements with acceptance testing, high-level design with system testing, down to unit testing against actual code. Test planning can run in parallel with development instead of waiting for it to finish.

Interviewers use this to catch candidates who think "testing" starts after a build is ready. The stronger answer reviews requirements for ambiguity and testability before a line of code exists. A requirement nobody can verify is a defect on its own, and it's far cheaper to catch on a whiteboard than in production.

Entry criteria are the conditions that must hold before testing starts: a stable build, test data loaded, test cases reviewed and ready. Exit criteria are the conditions before testing is considered done: a defined percentage of cases executed, no open blocker or critical defects, and remaining known issues formally accepted by the team, not quietly ignored.

Teams skip exit criteria under deadline pressure because they feel like an obstacle standing between the team and a release date. That's backwards. Skipping them doesn't remove risk, it moves the discovery of that risk from a test cycle into production, where it costs more to fix. A QA engineer who tells a release manager "we're not meeting exit criteria, here's what's still open" under real pressure is worth more than one who just writes test cases fast.

Equivalence partitioning groups inputs into classes that should behave the same way, so one representative value per class covers the whole class. Boundary value analysis then focuses on the edges of those classes, since bugs cluster at boundaries far more often than in the middle of a range.

Take an age field accepting 18 to 65. Partitioning gives three classes: below 18 (invalid), 18 through 65 (valid), above 65 (invalid), one case each. Boundary analysis adds the edges, 17, 18, 19, 64, 65, 66, and those six catch an off-by-one error the partitions miss. A developer who wrote age > 18 instead of age >= 18 passes every partition test and fails exactly one boundary case.

Severity measures how bad a defect is technically, does it crash the app, corrupt data, or just look wrong. Priority measures how urgently it needs fixing from a business standpoint, regardless of technical severity.

A classic divergence: a crash on a legacy browser used by 0.2% of traffic is high severity, a full crash, but low priority, almost nobody hits it. A misspelled word on the homepage headline is low severity, nothing breaks, but high priority, it's live right now. Candidates who confuse the two usually assume severity and priority move together, and plenty of real bugs prove that wrong.

Steps to reproduce, numbered and specific, not a paragraph of prose. Expected versus actual result, stated separately. Environment details: browser or app version, OS, device, and the test data state the bug depends on. A screenshot or recording whenever the bug is visual or intermittent. And the exact data used, since "enter an email" is useless if the bug only fires on an address with a plus sign in it.

The biggest cause of a "can't reproduce" bounce-back isn't a bad bug. It's a report missing the exact input or sequence, especially for state-dependent bugs where order matters as much as the actions themselves.

Response schema, does every field exist with the right data type. Response time, inside an agreed threshold, not just "eventually returned." Headers, is Content-Type correct, are caching headers set right. Error handling, does a malformed request return a 400 with a useful message instead of a 500 leaking a stack trace. And side effects, did the change actually happen downstream, not just in the response body.

Interviewers watch for candidates who only check the status code and happy path. Saying "I'd verify the resource exists by querying it separately" shows a response can lie about what really happened.

bash
curl -i -X POST https://api.example.com/v1/orders 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer $TOKEN" 
  -d '{"item_id": 4521, "quantity": 2}'

Checks beyond the 201 status: the Location header points to the new resource, the response body includes an order_id with status "pending", a follow-up GET on that order_id confirms it actually persisted, and repeating the exact same call with the same idempotency key returns the same order_id instead of creating a duplicate.

Group by the column you're checking, then filter groups with more than one row using HAVING, since WHERE can't filter on an aggregate like COUNT.

sql
SELECT email, COUNT(*) AS occurrences
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

The follow-up that catches people: what if duplicates differ by case, "Jane@x.com" versus "jane@x.com"? The query above treats those as different rows unless the column has case-insensitive collation. The fix is grouping on LOWER(email) instead, and a candidate who catches that unprompted is showing the attention that finds this bug before a customer does.

Row count reconciliation first, does the destination have the number of rows you'd expect given the source and filtering rules. Then a checksum comparison on a sample of records, source against destination directly, rather than trusting that a matching count means matching data. Spot-check a handful of records with NULL fields or special characters, since transformation logic tends to break there first.

The mistake weaker answers make is stopping at row counts. Two tables can have identical counts and still hold wrong data if a step silently truncated a field or mishandled a timezone. The count tells you nothing got dropped, nothing about whether what arrived is correct.

Start with smoke tests on the critical path, the handful of flows that would be catastrophic if broken: login, checkout, the core action the product exists to perform. These flows are stable, high-value, and repetitive, exactly the combination that makes automation pay off fastest.

What not to automate first: anything still actively changing, since a UI you automate today might get redesigned next sprint, one-off exploratory scenarios, and anything better suited to a human eye. Automating a feature still in flux is a common mistake, usually driven by pressure to show progress rather than what actually reduces risk.

Fast, stable tests, unit and smoke, run on every commit and gate the merge, a failure blocks the pull request. Slower suites, full regression, run on a schedule or against the main branch, since running everything on every commit makes the feedback loop too slow to be useful.

When a test fails, the pipeline should fail loudly and block deployment, not log a warning and move on. The harder call is telling a real failure apart from a flaky one. A QA engineer who reruns a failing test until it goes green, without investigating why it flaked, is training the team to ignore red builds, worse than having no automated tests at all.

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Run smoke suite
        run: npm run test:smoke
      - name: Run full regression
        run: npm run test:regression
        if: github.ref == 'refs/heads/main'

During sprint planning, QA reviews each story for testability before it's committed to, flagging ambiguous requirements and anything that can't reasonably be verified as written. QA also estimates testing effort alongside development effort, since a story small to build can still be large to test properly.

When acceptance criteria are vague, the fix isn't guessing and writing test cases anyway. Go back to the product owner before the sprint starts, or before execution begins, and get the ambiguity resolved in writing. A story that says "improve search" with no defined success metric isn't testable, and a QA engineer who ships cases against a guess sets up a disagreement about "done" later in the sprint, when it costs far more to resolve.

Start with the obvious: valid credentials succeed, invalid ones fail with a clear but not overly specific error message. Then widen out. Case sensitivity on the email field. Password behavior with special characters, very long input, and trailing whitespace from a copy-paste. Account lockout after repeated failed attempts, and what "repeated" means, per account, per IP, or both.

The cases that separate a strong answer from a checklist: a session already active in another tab when the user logs in again. Browser autofill submitting stale credentials right after a password reset. An SSO redirect where the identity provider errors mid-flow, does the app fail gracefully or hang on a blank screen. And a small one interviewers like: does Caps Lock get flagged before submission, a detail that shows up in real usability complaints more than test plans account for.

Functional basics first: a valid, active code applies the correct discount, an invalid one shows a clear error, an expired one gets rejected with a message that actually says "expired" instead of "invalid." Case sensitivity on the code is worth checking, plenty of implementations accidentally let "SAVE20" and "save20" behave differently.

Then the business-logic cases that matter for revenue: does the discount apply before or after tax, consistent with the receipt. Can a code stack with another, and if not, does the UI block it or let a second discount silently apply. What happens to a flat-dollar discount when the cart falls below it, floor at zero or go negative. Two devices redeeming a single-use code near the same moment, does the second attempt fail, or is there a race condition letting it get used twice, the same bug class as the transfer-funds scenario earlier. Decremented usage counts need the same protection decremented balances do.

A decision table lists every condition that affects the outcome as a column, and every combination of those conditions as a row, with the expected result at the end of each row. Say a loan approval rule depends on credit score above 650, income above a threshold, and existing debt below a ratio. Instead of trying to hold all the combinations in your head, you lay out true/false for each condition across rows, and immediately see there are eight combinations, not three.

The value is catching combinations nobody thought to test verbally, like good credit score but high debt ratio, which might get rejected even though 'credit score is fine' sounds like it should pass. It also becomes documentation: when the business rule changes six months later, you update the table and immediately see which rows, and which test cases, need to change. I reach for this specifically when a feature has three or more independent conditions driving different outcomes, because past that, ad hoc test case lists start missing combinations.

A flaky test passes and fails on the same code, with no actual bug involved, purely based on timing, environment, or execution order. Adding a retry makes the pipeline green but doesn't fix anything, it just hides the flakiness until it eventually gets bad enough to fail twice in a row.

The real fixes depend on the root cause. If it's waiting on an async UI update, swap a fixed sleep for an explicit wait on the actual condition you care about, like 'wait until this element is visible,' not 'wait 2 seconds and hope.' If it's shared state, like two tests both mutating the same seeded user, give each test its own isolated data. If it's order-dependent, meaning it only fails when run after a specific other test, that's usually leftover state or a shared mock that didn't get reset in teardown. My approach is to quarantine the test so it doesn't block the pipeline, but track it separately and actually dig into logs and timing rather than letting it rot in a 'known flaky, ignore' list, because that list only grows.

The test pyramid says you should have a large base of fast, cheap unit tests, a smaller layer of integration tests, and a thin top layer of slow, expensive end-to-end UI tests. The idea is that most bugs should be caught cheaply and fast, before you ever need to spin up a browser.

The ice cream cone is the pyramid flipped: a handful of unit tests, some integration tests, and a huge pile of UI automation trying to cover everything. It usually happens when developers aren't writing unit tests as part of their normal workflow, so QA is left to catch regressions the only way available, through the UI. The result is a suite that takes hours to run, is fragile against every UI tweak, and gives feedback so slowly that people stop trusting it and start merging around it. Fixing it isn't a QA-only job, it means pushing unit test ownership back onto the people writing the code, and reserving E2E tests for the handful of critical user journeys that actually need full-stack coverage.

Start with the obvious boundaries: a file at exactly the size limit, one byte over, a zero-byte file, and an enormous file that should time out or get rejected cleanly rather than crashing the upload handler. Then test type validation properly, meaning checking the actual file signature or magic bytes, not just the extension, because renaming a .exe to .jpg is the first thing anyone probing your upload endpoint tries.

Beyond that, test a corrupted or truncated file of a valid type, like a JPEG that cuts off halfway through, to see if the processing step fails gracefully instead of hanging. Check what happens with two users uploading files with the identical filename at the same time, and confirm the stored object's content-type header matches what actually gets served back, since a mismatch there is a real security issue if a browser decides to execute something it shouldn't. If there's a virus scanning step in the pipeline, test that a scan failure or timeout doesn't silently let the file through.

Contract testing, the Pact-style consumer-driven approach, has the consumer of an API define exactly what interactions it expects, request shape and response shape, as a contract. The provider then runs that contract against its actual implementation independently, without either service needing the other one running. If the provider changes a field name or removes an endpoint the consumer depends on, the contract test fails immediately, on the provider's own pipeline, before anything gets deployed.

End-to-end tests across a dozen services are slow, need a full environment stood up, and fail for reasons that have nothing to do with the thing you're actually verifying, a flaky dependency three services away breaks your test for service A. Contract tests isolate the actual risk, which is usually two teams changing an API's shape independently and not noticing until production. They don't replace E2E entirely, you still want a small number of true end-to-end smoke tests for the critical paths, but contract tests catch the much more common integration break far earlier and far cheaper.

Start with the basics: an exact match query, a partial match, a query with no results, and a query with special characters or SQL-injection-style input to make sure it's sanitized rather than erroring out or, worse, executing. Then layer in filters: does applying two filters combine with AND logic or OR logic, and is that actually what the product intends, because I've seen this get flipped by accident during a refactor and nobody notice for weeks.

Sorting needs its own attention around ties: if fifty results all have the identical timestamp, does the sort order stay stable across page reloads, or does it shuffle because the backend isn't using a deterministic secondary sort key. Also worth checking, does changing a filter reset pagination back to page one, or does a user end up on 'page 3' of a filtered result set that only has one page. And with any decent-sized dataset, check performance separately from correctness, a search that returns correct results in four seconds is still a failing test for most products.

Offset-based pagination, like 'skip 20, take 20,' has a specific and well-known bug: if a row gets inserted or deleted while a user is paging through results, the offset shifts under them, and they either skip a row entirely or see the same row twice on the next page. Cursor-based pagination avoids this by anchoring on a stable, unique value, like 'give me everything after id 4521,' so the position doesn't depend on how many rows exist before it.

To test this properly, don't just check that page 2 loads. Insert a new row between fetching page 1 and page 2 in an offset-based system and confirm whether the bug actually reproduces, that's the real test, not just 'does pagination render.' For cursor-based pagination, test what happens with a tampered or stale cursor value, an empty result set, and the exact boundary at the last page, where the 'next cursor' should come back null or empty rather than pointing to a page that returns nothing.

Page Object Model encapsulates the locators and actions for a given page or component into a single class, so your test code calls methods like loginPage.login(email, password) instead of finding a button by CSS selector directly inside the test. When the UI changes, a button's selector moves, a field gets renamed, you update it in one place, the page object, instead of hunting through every test file that happened to reference that element.

A minimal example:

javascript
class LoginPage {
  constructor(page) { this.page = page; }
  async login(email, password) {
    await this.page.fill('#email', email);
    await this.page.fill('#password', password);
    await this.page.click('button[type=submit]');
  }
}

Every test that needs to log in just calls loginPage.login(...) and reads clearly. Without this pattern, a single selector change in the login form can mean editing thirty test files, which is exactly how UI automation suites become something nobody wants to touch.

The core challenge is that there's no immediate return value to assert on, the work happens after the request finishes. You can't just call the endpoint and check the response, you have to poll or wait for the eventual side effect, like checking that a row's status flips from pending to processed, ideally with a bounded wait-and-retry rather than a fixed sleep that's either too short and flaky or too long and wastes time on every run.

Beyond the happy path, test what happens when the consumer receives the same message twice, since most queue systems guarantee at-least-once delivery, not exactly-once, so the consumer needs to be idempotent or you'll double-process. Test what happens after repeated failures, does the message eventually land in a dead-letter queue instead of retrying forever, and does something actually alert on that. If ordering matters for correctness, test the case where messages arrive out of order, because most queues don't guarantee strict ordering across partitions or shards, and code that silently assumes they do is a common source of production bugs.

Load testing checks behavior at the traffic level you actually expect in production, confirming response times and error rates stay acceptable under normal or peak expected usage. Stress testing pushes past that, deliberately increasing load until something breaks, to find the actual ceiling and see how it fails, gracefully with clear errors, or catastrophically by crashing the whole service and taking healthy requests down with it.

Soak testing, sometimes called endurance testing, runs a sustained, moderate load for an extended period, hours or days, not minutes. This is the one that catches problems load and stress testing miss entirely: a slow memory leak that looks fine for the first twenty minutes and only becomes visible after six hours, a connection pool that isn't releasing connections properly, or disk space quietly filling up from logs. I've seen teams skip soak testing because it's slow and unglamorous, and then get paged at 3am for exactly the class of bug it exists to catch.

You need both states actually tested, flag on and flag off, as separate paths, not just the new behavior. It sounds obvious, but I've seen teams thoroughly test the new flag-on path and completely forget the old flag-off path still needs to work identically to before, especially once other changes land on top of it while the flag sits there for weeks.

For a percentage rollout, test that the split is actually consistent per user, meaning the same user doesn't flip between the old and new experience on every page load, which is confusing and can corrupt session-dependent state. Confirm the kill switch actually works, meaning flipping the flag off mid-incident reverts behavior without needing a deploy, and test that under real conditions, not just in theory. If there are multiple flags active on the same feature area, check the combinations, because two flags that were each tested independently can still produce a broken combined state nobody anticipated. And build in a step to actually remove the flag and its dead code path once the rollout finishes, stale flags accumulate fast and each one is a hidden branch that still needs testing forever.

The first thing I check is whether the system stores timestamps in UTC and only converts to local time at the display layer, versus storing local time directly, because the second approach breaks the moment a user changes location or the server and client disagree on offset. Then I specifically test around daylight saving time transitions, since a scheduled job set for '2am local time' can run twice or not at all on the day clocks change, depending on the timezone.

Month and year boundaries deserve their own test cases too, adding one month to January 31st doesn't cleanly land on February 31st, and different libraries handle that rollover differently. For a global product, I test what 'today' means for a user in a timezone many hours offset from the server, since a report labeled 'today's activity' can quietly include or exclude hours depending on whose midnight you're using. Leap years come up less often but still bite date-math code that assumes February always has 28 days.

Hard questions

8

Risk-based testing. Rank cases by two factors: the probability something breaks, and the impact if it does. High-probability, high-impact cases, the payment flow, anything touching money, get tested no matter what. Low-probability, low-impact cases, a rarely used settings toggle, are first to cut, documented as accepted risk, not silently dropped.

What interviewers want to hear: you don't cut cases at random, and you tell whoever owns the release decision exactly what got cut. A QA engineer who quietly narrows coverage is more dangerous than one who says "we're shipping with these three areas untested, here's why, are you comfortable with that."

Start smaller than a full load test. Profile individual API endpoints under a moderate simulated load, even without dedicated tooling, to catch obvious problems first, an unindexed scan, a synchronous call that should be async, before worrying about precise numbers at scale.

Be upfront that results from a shared staging environment are directional, not definitive, since other traffic skews the numbers. The honest move is naming that limitation instead of presenting rough figures as exact, and flagging that a real launch decision needs an isolated environment first.

Mock or stub the third-party call for most of the suite, so tests aren't flaky because someone else's sandbox is down. Verify your own side of the contract, the request shape you send, and how you handle every response the third party could plausibly return: success, a 4xx, a 5xx, a timeout, a slow response near your own threshold.

Contract testing, Pact is the common example, adds a layer most candidates skip: both sides independently verify a shared contract, so a breaking change gets caught before a real customer hits it.

Trigger a change directly at the database level first, an UPDATE outside the application, and confirm the audit row has the correct before and after values, timestamp, and identifier. Then repeat through the application layer, since some apps route writes through an ORM in a way that could bypass the trigger, or fire it twice.

Test the edge cases: a bulk update touching a thousand rows in one statement, does the trigger fire once per row or miss rows, row-level and statement-level triggers behave differently here. A transaction that gets rolled back, does the audit entry roll back with it or incorrectly persist. Most trigger bugs live in exactly these two scenarios, not the single-row case everyone tests first.

The happy path is almost the easy part: request a reset, receive an email, click the link, set a new password, log in with it. The interesting bugs live in the states around that path.

Token expiry, does a reset link still work past its stated window, and does the error say "expired" instead of something misleading like "invalid request." Token reuse, clicking the same link twice shouldn't work the second time. Multiple resets in a row, does the oldest link invalidate when a new one issues, or do both stay valid, a real security gap if both. And the one candidates skip most: after a successful reset, do other active sessions get invalidated, or does someone already logged in stay in even after the legitimate user changed the password specifically to lock them out.

The classic example is inventory: two requests both check that stock is greater than zero, both pass the check, both decrement, and now stock is negative because the check-then-act wasn't atomic. To actually test this, I write a test that fires a batch of concurrent requests deliberately at the same resource, using something like a thread pool or concurrent async calls, not one request after another, and assert on the final state: stock should never go negative, and the number of successful orders should never exceed the number of units that existed.

This kind of bug often doesn't reproduce reliably in a small local test environment, because the timing window that causes the race might be a few milliseconds and your test happens to run the requests just far enough apart. So alongside the concurrency test, I review the actual code path for the fix, whether that's a database-level constraint that rejects the second decrement, a row-level lock, or an atomic increment/decrement operation, and then write a test that specifically confirms the second request gets a clean, handled rejection rather than a 500 error or silent overselling. Testing the failure mode is as important as testing that the race is prevented.

Start with the core contract: sending the exact same request twice with the same idempotency key should charge the customer exactly once, with the second response returning the original result rather than creating a second charge. I test that explicitly by firing the identical request twice back to back and confirming there's only one charge record and one transaction id in the downstream ledger, not just that the API returned 200 twice.

Then I test the boundary cases that actually cause incidents. Two requests with different idempotency keys but otherwise identical payloads should charge twice, because that's a legitimately different request, and if the system silently dedupes on payload alone instead of the key, that's its own bug. I test the client-retry scenario specifically, where the client times out waiting for a response but the charge actually succeeded server-side, then retries with the same key, confirming it gets the original success back instead of erroring or double-charging. I also check what happens if a webhook confirming the charge gets delivered twice, since that's a separate idempotency surface from the API call itself, and a naive webhook handler can double-credit an account even if the payment API itself was perfectly idempotent. Finally, I check what happens once an idempotency key expires and gets reused, since some implementations have a TTL on stored keys and reusing an old key after expiry should be treated as a brand new request.

I don't try to write a full test suite before touching anything, that's a multi-month project nobody will fund and it delays every other piece of work. Instead I write characterization tests first: black-box tests that capture what the system actually does right now, not what it's supposed to do, feeding in realistic inputs and locking in the current output as the expected result. That gives a safety net for refactoring even before anyone understands every internal detail of the code.

From there, I scope test-writing to whatever area I'm about to change, not the whole system, since testing code you're not touching yet just delays the actual work with no immediate payoff. I prioritize by risk, using whatever history is available, like which modules generate the most support tickets or hotfixes, and put tests there first. If the environment supports it, I'll also use shadow testing or traffic replay, sending a copy of real production requests to the new code path without letting its response actually reach the user, comparing outputs against the old path to catch behavioral differences before they ever hit a customer. That combination, characterization tests plus risk-based prioritization plus shadow comparison where possible, gets meaningful coverage in without needing a green light to freeze feature work for a quarter.

Real-time scenario questions

4

Start with the happy path: transfer a valid amount between two accounts owned by the same user, confirm both balances update and a transaction record gets created. Then work outward. Negative and zero amounts get rejected. An amount exceeding the available balance gets rejected cleanly, not partially deducted. Transferring to a closed or frozen account fails with a clear message. Currency mismatches need explicit handling, not a silent wrong-rate conversion.

The cases that separate a strong answer from a checklist: the network drops mid-transfer, after the debit but before the credit, does the system reconcile or does the money vanish. The same request gets submitted twice, a double-click or a retry, does it create a duplicate or get caught by an idempotency check. Two concurrent transfers from the same account that would overdraw it if both succeed, does a database constraint catch the race, or does a check-then-transfer sequence leave a gap. This is really testing whether you think about money the way engineers think about correctness under failure. Five obvious cases don't clear that bar.

First, I stop trying to test for a specific order and instead test that the system produces the correct final state regardless of order, which usually means the handlers need to be idempotent and commutative for the operations that matter. A concrete test is deliberately sending events B then A, when A is supposed to logically precede B, like an 'item added to cart' event arriving after 'checkout completed,' and confirming the system either buffers correctly, uses a sequence number or vector clock to detect and correctly reorder, or rejects the invalid state cleanly instead of corrupting data.

Second, I stop asserting with fixed sleeps and instead assert on eventual consistency with a bounded poll, wait up to some ceiling for the expected state, fail if it's not reached, but don't assume it happens instantly. Third, I deliberately inject delay and duplication in a test environment, using something like a chaos proxy or a queue configured to redeliver, rather than only testing the clean, well-ordered path that almost never matches production reality. The goal isn't proving ordering works, since you often can't guarantee that, it's proving the system degrades safely when ordering doesn't hold.

I start by listing what's actually different between CI and my machine, because 'it's flaky' is not a diagnosis. Common culprits: CI runners often have fewer CPU cores or throttled resources, which changes timing on anything with a race condition or a tight wait; CI containers frequently default to UTC while a laptop is set to local time, which breaks date-sensitive assertions; tests running in parallel on CI can share a database or port that isn't shared locally; and headless browser rendering can behave subtly differently from a headed browser, especially around viewport size and font rendering for visual assertions.

My actual process is to reproduce the CI environment as closely as possible, same Docker image, same environment variables, same parallelism settings, and run it locally inside that container rather than guessing. If it still only fails on the actual CI runner, I add much more logging and, for UI tests, capture a screenshot and the full page HTML at the point of failure, since 'element not found' with no context is nearly impossible to diagnose after the fact. I also check whether the test passes in isolation but fails as part of the full suite, which almost always points to shared state or leftover data from a test that ran before it, rather than anything wrong with the feature itself.

I build a small harness that simulates the third party rather than relying on their actual sandbox, since most sandboxes don't let you trigger retries or duplicates on demand. That harness needs to send the same event id twice and confirm my system dedupes it, meaning it processes the business effect once even though the HTTP call happened twice, usually by checking a processed-events table keyed on the event id before acting on it.

Next, I test out-of-order delivery on purpose, sending an 'order cancelled' event before the 'order created' event actually gets processed, since network retries and queueing on the sender's side mean strict ordering is rarely guaranteed even from a single provider. I also test that my endpoint correctly triggers the provider's retry behavior when it should, meaning a genuine failure on my side (a 500, a timeout) returns a status the provider interprets as 'retry me,' while a duplicate that I've already processed still returns a success status so the provider stops retrying something that actually succeeded. Last, I test signature verification with a tampered payload and an expired timestamp, since a webhook endpoint that skips or gets this wrong is an open door for anyone who finds the URL to fake events.

How to prepare for QA Engineer interview questions without wasting a month

Spend more time on test design and SQL than on memorizing tool syntax. Most QA Engineer interview questions repeat the same judgment, spot the edge case, explain your reasoning, defend a trade-off under pressure, far more than they test which automation framework you know.

Across mock QA interviews run through LastRoundAI's practice sessions, the SQL and defect-triage questions trip up more candidates than the automation questions do. It's not because the SQL is hard. It's because candidates rehearse automation talking points for weeks and walk in without touching a query editor. Practicing severity-versus-priority and SQL out loud closes that gap faster than another pass through a Selenium cheat sheet.

I don't have good data on how this differs inside large enterprise QA organizations, our own sessions skew toward candidates targeting startups and mid-size product companies, so treat anything here about a 200-person dedicated QA org with a grain of salt. What does look stable: demand for the role isn't going anywhere. The BLS puts median pay for software quality assurance analysts and testers at $102,610 as of May 2024, and PractiTest's 2026 State of Testing Report found test coverage and automation coverage are still the two most-tracked QA metrics industry-wide, whatever the hype cycle suggests about the role disappearing.

If you want to rehearse these QA Engineer interview questions out loud, including the follow-ups that separate strong answers from rehearsed ones, LastRoundAI's mock interview practice runs through SQL, defect triage, and scenario questions live. The free plan includes 15 credits a month that reset monthly, and Starter is $19/mo if more sessions would help. If prep isn't the bottleneck and finding enough matching QA openings is, Auto-Apply finds and applies to matching QA and test roles for you, with every application queued for your review before anything gets sent. Questions about either product: contact@lastroundai.com.

Leave a Reply

Your email address will not be published. Required fields are marked *