Interview prep · 150 questions

Automation testing interview questions

Selenium, Playwright, framework design and the flaky-test questions that separate someone who has maintained a suite from someone who has only written one.

50 questions

How do you decide what to automate and what to leave manual?

Automation Strategyjuniormidsenior

Automation is an investment. It has a build cost and — the part people forget — a permanent maintenance cost. It only pays back through repetition, so the decision is economic, not ideological.

Automation is an investment. It has a build cost and — the part people forget — a permanent maintenance cost. It only pays back through repetition, so the decision is economic, not ideological.

I automate tests that are repetitive (they run every release), stable (the feature isn't being redesigned next sprint), deterministic (same input, same output, no human judgement), and high risk (money paths, authentication, data integrity). Regression suites and smoke checks are the obvious winners.

I leave manual: one-off validations, features whose UI is still churning, anything requiring aesthetic or usability judgement, and tests where the setup cost dwarfs the risk being covered.

The sharpest version of this answer adds a second axis: not just whether to automate, but at which layer. A pricing rule doesn't need a browser — an API test verifies it faster, more reliably, and at a fraction of the maintenance cost. Most bloated, flaky UI suites exist because someone automated at the UI layer things that never belonged there.

So: automate what repeats, at the lowest layer that can catch the bug.

Key points
  • Automation = build cost + permanent maintenance cost, repaid only by repetition
  • Automate: repetitive, stable, deterministic, high-risk (regression, smoke, money paths)
  • Leave manual: one-offs, churning UI, aesthetic/usability judgement, setup cost > risk
  • Second axis: choose the LAYER — most flaky UI suites are API tests in the wrong place
They'll ask next
  • What's your automation coverage target, and why that number?
  • How do you handle a feature whose UI changes every sprint?
The trap

Saying 'automate everything' or quoting a coverage percentage as a goal. Both signal you've never maintained a large suite. The follow-up is always about maintenance cost.

Link to this question

What should NOT be automated?

Automation Strategyjuniormid

Several categories, and being able to name them is a maturity signal — anyone can list what to automate.

Several categories, and being able to name them is a maturity signal — anyone can list what to automate.

Exploratory testing, by definition. Its value comes from a human reasoning about what to try next; a script only checks what someone already thought of.

Usability and visual aesthetics. A machine can tell you the button exists at coordinates (x, y). It cannot tell you the flow is confusing or the layout looks broken to a human eye. Visual-regression tools help with pixel diffs, but they answer 'did it change?', not 'is it good?'

Rapidly-changing UI. If a screen is due to be rebuilt in a month, any script written against it today is already scheduled for deletion — you'd be paying twice for one piece of coverage.

One-off checks — a data migration verified once will never repay the scripting cost.

Tests with unstable or unavailable dependencies — if a third-party sandbox is down half the time, your automated test is a random number generator, and worse than nothing because it erodes trust in the whole suite.

Anything requiring genuine human judgement: does this error message actually make sense to a person?

The unifying principle: automate the check, not the thinking.

Key points
  • Exploratory testing — automation only checks what someone already imagined
  • Usability and aesthetics — a machine sees presence, not sense
  • Rapidly-changing UI, one-off checks, unstable dependencies
  • Anything needing human judgement (does this message make sense?)
  • Principle: automate the check, never the thinking
They'll ask next
  • Where does visual regression testing fit, then?
  • A manager wants 100% automation. How do you respond?
Link to this question

How do you calculate or argue the ROI of test automation?

Automation Strategymidsenior

The naive formula is: (manual execution time × number of runs) − (build time + maintenance × runs). If that's positive, it pays back.

The naive formula is: (manual execution time × number of runs) − (build time + maintenance × runs). If that's positive, it pays back. It's a useful starting frame because it makes the maintenance term visible, which is where most automation business cases quietly lie.

But I'd argue ROI on more than saved hours, because saved hours is the weakest case and a manager can always say 'testers are cheaper than engineers'.

The stronger arguments: speed of feedback — a regression pack that runs in 20 minutes on every pull request changes developer behaviour; a 3-day manual pass changes nothing because nobody waits for it. Release frequency — you cannot ship weekly with a 3-day manual regression; automation is the enabler of the business's actual goal. Consistency — a machine executes step 47 identically at 5pm on a Friday; a human doesn't. Escaped defect reduction, measured over quarters — the number a CFO understands. And freeing human testers for exploratory work, which finds the defects automation structurally cannot.

The honest close: automation rarely reduces headcount, and promising that is how automation programmes lose credibility. It changes what the team can do, not how many people you need.

Key points
  • Base formula: (manual time × runs) − (build + maintenance × runs) — makes maintenance visible
  • Stronger arguments than saved hours: feedback speed, release frequency, consistency, escaped-defect reduction
  • Automation enables the business goal (ship weekly), it isn't the goal
  • Frees humans for exploratory work that automation cannot do
  • Don't promise headcount reduction — that's how automation programmes lose credibility
They'll ask next
  • Your suite takes 3 engineers to maintain. Justify it to a CFO.
  • How do you measure whether your automation is actually finding bugs?
Link to this question

Explain Selenium's architecture. What actually happens when you call driver.click()?

Selenium Coremidsenior

Selenium is a client–server system, and understanding that explains most of its behaviour.

Selenium is a client–server system, and understanding that explains most of its behaviour.

Your test code uses a language binding (Java, Python, C#…). That binding doesn't talk to the browser directly. It serialises your command into an HTTP request following the W3C WebDriver protocol and sends it to a driver executable — ChromeDriver, geckodriver, and so on. That driver is browser-specific: it knows how to translate a standard protocol command into whatever automation interface that particular browser exposes. The driver executes the action, and the result travels back up the same chain as an HTTP response.

So driver.click() becomes roughly: your binding builds a POST to /session/{id}/element/{elementId}/click, ChromeDriver receives it, instructs Chrome to dispatch the click at the element's location, and returns success or an error.

Two consequences worth stating, because they're what the interviewer is really after. First, every command is a network round trip — which is why Selenium is slower than in-browser tools like Cypress and why chatty tests with hundreds of tiny commands crawl. Second, because the protocol is a standard (W3C), the same test code works across browsers by swapping the driver — that portability is Selenium's core value proposition.

Key points
  • Client–server: language binding → W3C WebDriver protocol over HTTP → browser-specific driver → browser
  • click() becomes an HTTP POST to a WebDriver endpoint; the driver translates it for that browser
  • Every command is a network round trip — hence the latency versus in-browser tools
  • W3C standardisation is why one test runs across browsers by swapping the driver
They'll ask next
  • Why is Cypress faster, and what does it give up in exchange?
  • What changed when WebDriver became a W3C standard?
Link to this question

What locator strategies does Selenium support, and how do you choose?

Locatorsjuniormid

The available strategies are ID, name, class name, tag name, link text, partial link text, CSS selector and XPath.

The available strategies are ID, name, class name, tag name, link text, partial link text, CSS selector and XPath.

My priority order, and the reasoning behind it:

ID first — if it's genuinely unique and stable, it's the fastest and most readable locator there is. Then a dedicated test attribute (data-testid, data-qa). This is the one I actually push for, because it's the only locator a developer will never break by accident: it exists solely for testing, so refactoring CSS or restructuring the DOM doesn't touch it. Then CSS selectors, which are concise, fast, and readable. XPath last, and only when I need something CSS can't express — most commonly traversing to a parent, or locating by visible text.

What I avoid: locators tied to styling (class="btn btn-primary mt-4" will change the moment a designer touches it), auto-generated IDs like ember1042, and anything positional such as div[3]/span[2], which breaks the instant someone inserts an element.

The senior version of this answer isn't a ranking at all — it's that the durable fix is a team convention: agree data-testid with the developers and the whole class of locator flakiness disappears. That's a collaboration answer, not a tooling one.

Key points
  • Priority: ID → data-testid → CSS → XPath (last resort)
  • data-testid is the only locator a developer won't break by accident
  • Avoid: style-based classes, auto-generated IDs, positional paths like div[3]
  • The real fix is a team convention with developers, not a cleverer selector
They'll ask next
  • The developers refuse to add test IDs. What now?
  • Give me a case where only XPath will do.
The trap

Naming XPath as your first choice, or listing locators without a priority order. The strongest answer isn't a ranking at all — it's proposing a data-testid convention with the developers.

Link to this question

CSS selectors vs XPath — which do you prefer and why?

Locatorsjuniormid

I default to CSS and reach for XPath when CSS can't express what I need.

I default to CSS and reach for XPath when CSS can't express what I need.

CSS is more concise, generally faster (browsers have native, heavily-optimised CSS engines), more readable to developers, and better supported across older browsers. #login button.primary is instantly comprehensible.

XPath can do things CSS cannot: traverse upwards to a parent or ancestor, and select by visible text. //button[text()='Confirm booking'] has no CSS equivalent, and axes like following-sibling or ancestor:: are genuinely useful in awkward, unstructured DOMs (tables are the classic case — find the row containing this text, then click the button in that row).

The performance gap between the two is often overstated. On a modern browser it's negligible for typical page sizes, and I wouldn't design my locator strategy around it. What actually matters is stability and readability, and by those criteria a well-written XPath beats a brittle CSS selector every time.

What I do avoid, in either language, is absolute or positional paths. /html/body/div[2]/div[3]/span is not a locator, it's a time bomb.

Key points
  • Default CSS: concise, natively optimised, readable, well-supported
  • XPath for what CSS can't do: traverse to parent/ancestor, select by visible text, axes
  • Performance difference is largely overstated on modern browsers — don't design around it
  • Judge by stability and readability, not language; avoid absolute/positional paths in either
They'll ask next
  • Write an XPath that finds a table row containing a given text and clicks the delete button in it.
  • When is text-based locating a bad idea?
Link to this question

What is the difference between absolute and relative XPath?

Locatorsjunior

An absolute XPath starts from the document root and specifies the complete path down to the element: /html/body/div[1]/div[3]/form/input[2]. It begins with a single slash.

An absolute XPath starts from the document root and specifies the complete path down to the element: /html/body/div[1]/div[3]/form/input[2]. It begins with a single slash.

A relative XPath starts anywhere in the DOM and matches on characteristics rather than position: //input[@data-testid='email']. It begins with a double slash.

Absolute XPath is almost always the wrong choice, and this question exists to check you know why. It encodes the entire structure of the page into the locator. Insert one wrapper div for a styling change — something a developer does without a second thought — and every absolute locator below it breaks. The test failure then tells you nothing about the product; it tells you about the DOM.

Relative XPath depends only on the element's own attributes or its relationship to a nearby stable anchor, so it survives restructuring.

The only time I'd tolerate an absolute path is a throwaway debugging expression in the console. It should never reach a committed test. When people say their Selenium suite is 'flaky', a startling proportion of the time the real cause is locators like these, and the fix is a locator strategy, not a longer wait.

Key points
  • Absolute: starts at root with a single slash, encodes the full DOM path — brittle
  • Relative: double slash, matches on attributes or a stable anchor — resilient
  • One inserted wrapper div breaks every absolute path below it
  • Absolute paths are a common hidden cause of 'flaky' suites — the fix is locators, not waits
They'll ask next
  • How do you locate an element that has no unique attributes at all?
  • What makes a good 'anchor' element to locate relative to?
The trap

Not knowing why absolute XPath is dangerous. It encodes the entire DOM structure into your locator — one inserted wrapper div breaks everything below it.

Link to this question

How do you handle dynamic elements whose IDs change on every page load?

Locatorsmid

Layered, from best fix to last resort.

Layered, from best fix to last resort.

First, fix it at the source. Ask the developers for a stable data-testid. This is not a cheeky request — it takes them thirty seconds, it costs nothing at runtime, and it permanently removes a category of failure. If your organisation lets you make it a convention, you've solved the problem rather than working around it forever.

Second, match the stable part. Dynamic IDs are usually partially predictable: user_input_8823 becomes //input[starts-with(@id,'user_input_')], or contains(@id,'input'). CSS supports the same idea with [id^='user_input_'].

Third, anchor to something stable nearby. The element itself may be dynamic, but its label, its container, or its heading usually isn't. Locate the stable ancestor and traverse: find the section headed 'Payment', then the input inside it. This is where XPath axes earn their keep.

Fourth, use semantic attributes: aria-label, name, placeholder, role. These tend to be stable because changing them changes behaviour or accessibility, not just appearance.

What I don't do is add a longer wait and hope. A wait fixes timing, not identity — if the locator is wrong, waiting longer just fails more slowly.

Key points
  • Best fix: get a stable data-testid from developers — it's a thirty-second change
  • Match the stable portion: starts-with / contains in XPath, ^= in CSS
  • Anchor to a stable ancestor or sibling and traverse to the dynamic element
  • Use semantic attributes (aria-label, name, role) — they're stable because they carry meaning
  • A longer wait never fixes a wrong locator; it just fails more slowly
They'll ask next
  • The whole DOM is auto-generated with no stable attributes. Now what?
  • How would you locate a row in a data grid that's virtualised?
Link to this question

Explain implicit, explicit and fluent waits. Which do you use?

Waits & Synchronisationjuniormidsenior

Implicit wait is a global setting: for the rest of the session, when an element isn't found, poll for up to N seconds before throwing. One line of configuration, applies everywhere.

Implicit wait is a global setting: for the rest of the session, when an element isn't found, poll for up to N seconds before throwing. One line of configuration, applies everywhere.

Explicit wait targets a specific condition on a specific element: wait up to N seconds until this element is clickable, visible, or contains this text. It waits for the state you actually need, not merely for existence.

Fluent wait is an explicit wait with finer control — you set the polling interval and the exceptions to ignore during polling.

I use explicit waits, and I don't mix them with implicit ones.

The reason is the crux of the question. Implicit wait only waits for the element to be present in the DOM. But present is not the same as ready: an element can exist while still hidden, still disabled, still covered by a loading overlay, or still animating into position. Selenium happily returns it, you click, and the click lands on the spinner. That's a huge share of real-world 'flakiness'.

Worse, mixing implicit and explicit waits produces unpredictable compound timeouts — the documented behaviour is essentially 'don't do this'. So: pick explicit, everywhere.

Key points
  • Implicit: global, waits for DOM presence only
  • Explicit: per-condition (clickable, visible, text present) — waits for readiness, not existence
  • Fluent: explicit with custom polling interval and ignored exceptions
  • Present ≠ ready: hidden, disabled, or overlay-covered elements pass an implicit wait and fail your click
  • Never mix implicit and explicit — the compound timeout behaviour is unpredictable
They'll ask next
  • Why exactly does mixing implicit and explicit waits cause problems?
  • How does Playwright's auto-waiting remove this whole class of bug?
Link to this question

Why is Thread.sleep() (or time.sleep()) considered bad practice?

Waits & Synchronisationjunior

Because it's a guess, and it's wrong in both directions.

Because it's a guess, and it's wrong in both directions.

If you sleep for 5 seconds and the element appears in 200 milliseconds, you have donated 4.8 seconds to every run of that test. Multiply by hundreds of tests and your suite takes an hour when it should take ten minutes — and a slow suite is one people stop running, which is how automation dies quietly.

If the environment is slow that day and the element takes 6 seconds, your test fails anyway. So the sleep didn't even buy you reliability; it just moved the failure.

A proper explicit wait is a conditional wait: it polls, and it proceeds the instant the condition is true, up to a maximum. Fast when the app is fast, patient when it isn't. Strictly better on both axes.

Sleeps are also a lie in a code review — they hide the fact that nobody knows what the test is actually waiting for.

The one narrow exception I'd defend: a short sleep as a temporary diagnostic while debugging locally, or as a documented last resort against a genuinely unobservable third-party animation. In committed test code, a sleep should be treated as a defect with a comment attached.

Key points
  • A sleep is a guess: too long wastes time on every run, too short still fails
  • Slow suites stop being run — that's how automation programmes die
  • Explicit waits are conditional: they proceed the instant the condition is true, up to a max
  • Sleeps hide the fact that nobody knows what the test is waiting for
  • Acceptable only as a temporary local debugging aid, never in committed code
They'll ask next
  • What would you wait for instead of sleeping after clicking 'Submit'?
  • How do you wait for something with no visible DOM change — an analytics call, say?
The trap

Hard-coded sleeps are the single most common reason take-home automation assignments get rejected. Reviewers scan for them first.

Link to this question

What causes a StaleElementReferenceException and how do you fix it?

Waits & Synchronisationmid

It means you're holding a reference to an element that no longer exists in the DOM as you knew it. You found the element, the page changed — a re-render, an AJAX update, a framework re-mount — and…

It means you're holding a reference to an element that no longer exists in the DOM as you knew it. You found the element, the page changed — a re-render, an AJAX update, a framework re-mount — and the reference you're holding now points at a detached node. Selenium refuses to act on it.

The classic trigger is exactly the pattern people write first: find a list of rows, loop over them, and click something in each. The first click causes a re-render; every remaining reference in your list is now stale.

The fixes, in order of preference. Re-find the element immediately before you use it — don't cache references across an action that changes the page. In the loop case, re-fetch the collection each iteration, or locate by index each time.

Wait for the condition that follows the change, not just for the element — wait for the spinner to disappear or for the new content to be present before re-locating.

Retry the interaction once on staleness in a small helper — acceptable, but treat it as a smell rather than a solution.

The conceptual point: stale element is rarely a Selenium bug. It's an application that re-renders and a test that assumed it wouldn't. Single-page apps trigger it constantly.

Key points
  • You hold a reference to a node the DOM has replaced (re-render, AJAX, framework re-mount)
  • Classic trigger: find a list, loop, click — the first click re-renders and staleness cascades
  • Fix: re-find immediately before use; never cache references across a page-changing action
  • Wait for the post-change condition, then re-locate
  • A retry helper is acceptable but is a smell, not a solution — SPAs cause this constantly
They'll ask next
  • Write the loop-over-rows pattern in a way that doesn't go stale.
  • How is this different from a NoSuchElementException?
Link to this question

What is the Page Object Model and why use it?

Page Object Modeljuniormid

The Page Object Model puts the locators and interactions for a page (or a component) into a dedicated class.

The Page Object Model puts the locators and interactions for a page (or a component) into a dedicated class. The test then talks in business language — loginPage.loginAs(user) — rather than in driver.findElement(By.id(...)) calls.

Two problems it solves.

Maintenance. A locator lives in exactly one place. When the login button's ID changes, you edit one line, not forty test files. Without POM, a single UI change can cost a day of mechanical edits — and that cost, repeated, is what makes teams abandon their suite.

Readability. A test reads as a sequence of user intentions, so a product owner can follow it and a new joiner can understand the flow without decoding selectors.

The part that separates a strong answer: knowing what does not belong in a page object. Assertions stay in the test. A page object exposes state and actions; it doesn't decide what's correct. Put assertions inside page objects and you've hidden your test's intent inside your infrastructure, and you can no longer reuse the page for a negative scenario.

Page methods should return either data, or the next page object — which is how you model navigation without hard-coding flows into the tests.

Key points
  • Locators and interactions live in a page/component class; tests speak business language
  • Maintenance: a locator changes in one place, not across forty test files
  • Readability: tests read as user intentions, reviewable by non-engineers
  • Assertions belong in the TEST, not the page object — pages expose state and actions
  • Page methods return data or the next page object, modelling navigation
They'll ask next
  • What exactly do you keep out of a page object?
  • How do you model a component that appears on many pages, like a nav bar?
The trap

Putting assertions inside page objects. It feels tidy and it's wrong: it hides the test's intent inside infrastructure and makes the page unusable for negative scenarios.

Link to this question

What belongs in a page object and what belongs in the test?

Page Object Modelmidsenior

The dividing line is: page objects know how, tests know what and why.

The dividing line is: page objects know how, tests know what and why.

In the page object: locators; low-level interactions (type, click, select); waits needed to make those interactions reliable; and composite actions expressed in domain language (searchForFlight(origin, destination, date)). Also methods that expose stategetErrorMessage(), isSubmitEnabled(), getDisplayedPrice().

In the test: the scenario, the data, and the assertions. The test decides that an error message should appear and that it should say a particular thing.

The most common mistake is asserting inside page objects. It feels convenient — loginPage.verifyErrorShown() — but it does three bad things. It hides the test's intent inside infrastructure, so you can no longer read a test and know what it checks. It couples the page to one scenario, so you can't reuse it where the error shouldn't appear. And it makes failures harder to read, because the failure surfaces from the wrong layer.

Second most common mistake: putting test data or environment logic in page objects. A page object should not know which environment it's on, nor which user is logging in. Both are inputs from the test.

Key points
  • Page objects know HOW; tests know WHAT and WHY
  • Page object: locators, interactions, necessary waits, domain-level actions, state exposure methods
  • Test: scenario, data, and all assertions
  • Asserting inside page objects hides intent, prevents reuse in negative cases, and obscures failures
  • Page objects must not own test data or environment knowledge
They'll ask next
  • Show me a page-object method signature you'd consider well-designed.
  • How do you handle a wait that's needed by every page?
Link to this question

Walk me through the architecture of an automation framework you have built.

Framework Designmidsenior

I'd structure the answer in five parts, because this question is really testing whether you can reason about design rather than list folders.

I'd structure the answer in five parts, because this question is really testing whether you can reason about design rather than list folders.

Context — what the app was, the release cadence, the team size. Design decisions are only defensible relative to constraints.

Stack — for example Python, pytest, Playwright for UI, requests for API, Allure for reporting.

Layers, and why each exists. A core/driver layer owning browser setup and teardown. A pages/components layer holding locators and interactions. An API clients layer, because the fastest way to set up state for a UI test is usually to create it via the API rather than clicking through six screens. A test data layer — factories producing unique data per test, so tests can run in parallel without colliding. A config layer so the same suite runs against local, staging and CI by changing an environment variable, never a code edit. A tests layer that contains only scenarios and assertions. A reporting layer with screenshots, traces and logs attached automatically on failure.

CI integration — what runs on a pull request versus nightly, and where the artifacts go.

One hard problem I solved — the flakiness, the parallel data collision, the auth token handling. That story is what they remember.

Key points
  • Answer in five parts: context → stack → layers (with rationale) → CI → one hard problem solved
  • Layers: core/driver, pages, API clients, test data factories, config, tests, reporting
  • API-based state setup for UI tests is a strong senior signal
  • Config by environment variable, never a code change, so one suite runs everywhere
  • The 'hard problem I solved' story is what the interviewer actually remembers
They'll ask next
  • Why that pattern and not another?
  • What would break first if the suite grew to 2,000 tests?
  • How does test data work when 20 tests run in parallel?
The trap

Listing a folder structure with no reasoning. Every layer you name, be ready to justify — and be ready to say what you'd do differently now.

Link to this question

What are the main types of automation framework?

Framework Designjuniormid

The traditional taxonomy, roughly in order of how they evolved:

The traditional taxonomy, roughly in order of how they evolved:

Linear / record-and-playback — recorded scripts, no structure. Fast to produce, impossible to maintain, and duplicated everywhere. Fine for a demo, fatal at scale.

Modular — the application is broken into reusable functions, and scripts call them. The first real step toward maintainability.

Data-driven — test logic is separated from test data, so the same script runs across many datasets held in CSV, Excel, JSON or a database.

Keyword-driven — actions are abstracted into keywords (Login, Search, Verify) held in a table, with the intent that non-programmers can compose tests. In practice, the keyword layer is often more expensive to maintain than the code it replaced.

Hybrid — combining data-driven and modular with page objects. This is what most real frameworks actually are.

BDD — Gherkin scenarios binding to step definitions, aimed at shared understanding with the business.

The answer that scores: modern frameworks don't really pick one. A typical Python or Java framework today is page objects plus data-driven parametrisation plus a runner's fixtures — which is 'hybrid' by this taxonomy, and the label is less interesting than the design rationale.

Key points
  • Linear/record-playback → modular → data-driven → keyword-driven → hybrid → BDD
  • Data-driven separates logic from data; keyword-driven abstracts actions into a table
  • Keyword-driven often costs more to maintain than the code it replaced
  • Most real modern frameworks are hybrid: POM + parametrised data + runner fixtures
  • The taxonomy matters less than being able to justify your design choices
They'll ask next
  • Which of these have you actually worked in?
  • Why do you think keyword-driven frameworks fell out of favour?
Link to this question

How do you implement data-driven testing?

Data-Driven Testingjuniormid

The core idea is one test body, many datasets. The test logic is written once; the inputs and expected outputs come from outside it.

The core idea is one test body, many datasets. The test logic is written once; the inputs and expected outputs come from outside it.

Mechanically, in pytest that's @pytest.mark.parametrize, feeding tuples of input and expected result; in TestNG it's a @DataProvider; in JUnit 5 it's @ParameterizedTest with a source. The data itself can be inline (best for a handful of cases — it keeps the test readable and self-contained), or external in CSV, JSON, YAML or a database (better when the dataset is large, or when non-engineers need to add cases).

Two practical points that separate a considered answer.

Each dataset must produce an independently-reported test. If ten datasets collapse into one test that stops at the first failure, you've lost most of the value — you want to know that cases 3 and 7 failed and the rest passed. Modern runners do this by default; older hand-rolled loops don't.

Don't confuse data-driven with data-dependent. Data-driven means the inputs vary. It does not mean tests should depend on pre-existing rows in a shared database — that's a different thing, and it's how suites become order-dependent and flaky. Generate or seed your data per test.

Key points
  • One test body, many datasets: parametrize (pytest), DataProvider (TestNG), ParameterizedTest (JUnit 5)
  • Inline data for a few cases (readable); external CSV/JSON/DB when large or business-owned
  • Each dataset must report as its own test — otherwise one failure masks the rest
  • Data-driven ≠ depending on pre-existing shared DB rows, which causes order-dependence and flakiness
They'll ask next
  • Where do you draw the line between inline and external test data?
  • How do you keep parametrised test names readable in the report?
Link to this question

Explain TestNG annotations and the order in which they execute.

Test Runnersjuniormid

TestNG's lifecycle annotations nest from broadest to narrowest scope.

TestNG's lifecycle annotations nest from broadest to narrowest scope.

@BeforeSuite runs once before everything. Then @BeforeTest (per <test> tag in the XML), then @BeforeClass (once per class), then @BeforeMethod (before every test method). Then each @Test. Afterwards the corresponding @After... annotations fire in reverse order: @AfterMethod, @AfterClass, @AfterTest, @AfterSuite. There's also @BeforeGroups/@AfterGroups for group-scoped setup.

The practical mapping matters more than the recital. @BeforeSuite is where you do genuinely one-off, expensive setup — start a Grid, spin up a container, load config. @BeforeMethod is where you create a fresh browser session or reset state, because each test must start from a known state and must not depend on the previous one.

That's the design principle behind the whole lifecycle, and it's what the interviewer is checking: shared state across tests is the origin of order-dependence, which is the origin of flakiness. If your tests only pass when run in a particular order, you have a design bug, not a timing bug.

Other useful ones: priority for ordering, dependsOnMethods for genuine dependencies (use sparingly), groups for selective runs, enabled=false to skip.

Key points
  • Order: BeforeSuite → BeforeTest → BeforeClass → BeforeMethod → @Test → AfterMethod → AfterClass → AfterTest → AfterSuite
  • BeforeSuite = expensive one-off setup; BeforeMethod = per-test fresh state
  • Design principle: every test starts from a known state and depends on no other test
  • Order-dependent tests are a design bug, and a leading cause of flakiness
  • Also: priority, dependsOnMethods (sparingly), groups, enabled=false
They'll ask next
  • When would dependsOnMethods be justified?
  • What's the equivalent lifecycle in pytest?
The trap

Reciting annotation order with no reasoning. What's being tested is whether you understand WHY the lifecycle exists — every test must start from a known state and depend on no other test.

Link to this question

What are pytest fixtures and conftest.py?

Test Runnersjuniormid

A fixture is a reusable piece of setup (and teardown) that pytest injects into a test by name. You declare def test_checkout(logged_in_page) and pytest finds the fixture called logged_in_page, runs…

A fixture is a reusable piece of setup (and teardown) that pytest injects into a test by name. You declare def test_checkout(logged_in_page) and pytest finds the fixture called logged_in_page, runs it, and passes you the result. Teardown lives after a yield in the same function, so setup and cleanup sit together instead of being split across two methods.

Fixtures compose: one fixture can request another, so logged_in_page can depend on browser, which depends on config. That composition is what makes them more flexible than classic before/after hooks.

They also have scopesfunction (default, fresh per test), class, module, session. Scope is a real design decision: a session-scoped browser is fast but risks state leaking between tests; a function-scoped browser is slower but hermetic. I default to function scope and only widen it for genuinely expensive, genuinely stateless resources.

conftest.py is where fixtures live when they're shared. Pytest discovers it automatically, with no import needed, and it applies to every test in that directory and below. That directory scoping is the point: a conftest.py at the root holds global fixtures, and one inside tests/api/ can hold API-only fixtures.

It's also where you put hooks, plugins, and command-line options.

Key points
  • Fixtures are named, injectable setup/teardown; teardown lives after yield
  • Fixtures compose — one can depend on another (config → browser → logged_in_page)
  • Scopes: function (default) / class / module / session — a real trade-off between speed and isolation
  • conftest.py is auto-discovered, needs no import, and applies to its directory and below
  • conftest.py also hosts hooks, plugins and custom CLI options
They'll ask next
  • When would you use a session-scoped fixture, and what's the risk?
  • How do you share a fixture across two different test directories?
Link to this question

TestNG vs JUnit — what are the meaningful differences?

Test Runnersmid

Historically the gap was large; with JUnit 5 it has narrowed considerably, and a candidate who still cites JUnit 4 limitations as though they're current is showing their age.

Historically the gap was large; with JUnit 5 it has narrowed considerably, and a candidate who still cites JUnit 4 limitations as though they're current is showing their age.

TestNG was built with functional and end-to-end testing in mind. Out of the box it gives you flexible parallel execution configured in XML, @DataProvider for data-driven tests, test grouping, dependsOnMethods, priorities, and built-in HTML reports. The XML suite file makes it easy to define different runs (smoke, regression) without touching code.

JUnit was built for unit testing and was more minimal by design. JUnit 5 added most of what people missed: @ParameterizedTest, tags (equivalent to groups), nested tests, extensions, and parallel execution.

My practical read: for UI and API test suites in Java, TestNG still has slightly better ergonomics for the things those suites need — parallelism config and data providers in particular. For unit tests, JUnit is the default and integrates most naturally with build tools and IDEs.

But I'd be honest in an interview that this is not a decision worth agonising over. Both are competent. Framework design — layering, data management, wait strategy — determines whether your suite survives; the choice between these two runners does not.

Key points
  • TestNG designed for functional/E2E: XML-configured parallelism, DataProvider, groups, dependencies, priorities
  • JUnit designed for unit tests; JUnit 5 closed most of the gap (ParameterizedTest, tags, extensions, parallelism)
  • TestNG has better ergonomics for suite-level parallelism and data providers
  • Honest close: this choice matters far less than framework design does
They'll ask next
  • How do you configure parallel execution in TestNG?
  • What in JUnit 5 changed your view of it?
Link to this question

What is the difference between hard and soft assertions, and when do you use each?

Assertionsjuniormid

A hard assertion stops the test immediately on failure. Everything after it is skipped. A soft assertion records the failure and lets the test continue, reporting all failures together at the end (in…

A hard assertion stops the test immediately on failure. Everything after it is skipped. A soft assertion records the failure and lets the test continue, reporting all failures together at the end (in TestNG you must call assertAll(), or the failures are silently swallowed — a genuinely common bug).

Use hard assertions when continuing makes no sense. If the login failed, asserting on the dashboard's contents produces a cascade of meaningless failures that obscure the actual cause. Fail fast, report the real problem.

Use soft assertions when you're validating multiple independent facts about one state and you want the full picture in one run. Checking fifteen fields on a rendered invoice is the classic case: with hard assertions you fix field 3, re-run, discover field 7 is also wrong, fix, re-run. With soft assertions you learn all fifteen results at once.

The design guidance I'd give: prefer one logical assertion per test, where 'logical' can be several soft assertions about the same fact. A test that asserts twenty unrelated things isn't a test, it's a report — and when it fails you can't tell from the name what broke. The name of a good test tells you what's wrong before you open the log.

Key points
  • Hard: stops the test at first failure. Soft: collects failures and continues (must call assertAll)
  • Hard for preconditions — a failed login makes downstream assertions meaningless noise
  • Soft for many independent facts about one state (validating fields on an invoice)
  • Aim for one logical assertion per test; a test asserting twenty unrelated things is a report, not a test
  • Forgetting assertAll() silently swallows soft failures — a classic bug
They'll ask next
  • What makes a good assertion failure message?
  • How many assertions is too many in one test?
Link to this question

What makes a good assertion?

Assertionsmid

A good assertion is specific, meaningful, and fails informatively.

A good assertion is specific, meaningful, and fails informatively.

Specific: assert the thing you actually care about, not a proxy for it. Asserting that a success <div> exists is weaker than asserting it contains the right order number — the div can render empty.

Meaningful: it must be able to fail. This sounds absurd, but assertions that can never fail are everywhere — asserting an element is present immediately after a wait that already required it to be present, or asserting response is not None and calling it an API test. If a test cannot fail, it is not a test; it's a costly no-op that inflates your coverage number and protects nothing. Mutation testing exists precisely to catch these.

Fails informatively: when it breaks at 3am in CI, the message alone should tell you what's wrong. assertEquals(expected, actual, "Order total after 10% promo") beats a bare assertTrue(x), which tells the next engineer nothing except that something, somewhere, was false.

One more: assert on state, not on implementation. Asserting that a particular API call fired couples your test to how the feature works today. Asserting that the balance decreased by £20 survives a refactor — and it's what the user actually cares about.

Key points
  • Specific: assert the real outcome, not a proxy (the order number, not just 'a div exists')
  • Meaningful: it must be able to fail — assertions that can't fail are worse than no test
  • Informative on failure: the message alone should explain what broke, at 3am, in CI
  • Assert on observable state, not implementation details — that survives refactoring
They'll ask next
  • How would you find tests in your suite that can never fail?
  • What is mutation testing and how does it relate to this?
Link to this question

What causes flaky tests?

Flaky Testsmidsenior

Flakiness is almost never random; it's a race condition you haven't identified yet. The causes cluster into a few families.

Flakiness is almost never random; it's a race condition you haven't identified yet. The causes cluster into a few families.

Timing and synchronisation — the biggest by far. Hard sleeps, implicit waits that check presence instead of readiness, and clicks that land on a spinner or an element still animating into place.

Shared state and test order-dependence. Test A creates a user that test B assumes exists; run them in a different order, or in parallel, and it collapses. Any suite that only passes in one order is already broken; parallelism just reveals it.

Test data collisions. Two parallel tests register the same email address. One wins.

Environment instability — a flaky third-party sandbox, a slow shared staging box, network blips.

Non-deterministic application behaviour — animations, lazy loading, polling, retries, race conditions in the app itself. And this one deserves emphasis: sometimes the test is right and the application is genuinely flaky. That's a real defect you've just found, and dismissing it as 'flaky test' hides a production bug.

Time and locale — tests that pass except near midnight, at month end, or in another timezone.

The most damaging effect isn't the failed run. It's that people stop believing red builds, and a suite nobody trusts is worse than no suite at all.

Key points
  • Timing/synchronisation: sleeps, presence-not-readiness waits, clicks landing on spinners
  • Shared state and order-dependence — parallelism reveals it, it doesn't cause it
  • Test data collisions between parallel runs
  • Environment instability and flaky third-party sandboxes
  • Sometimes the APP is genuinely flaky — that's a real defect, not a test problem
  • Worst effect: people stop trusting red builds
They'll ask next
  • How do you tell a flaky test from a genuine intermittent product bug?
  • You have 50 flaky tests and the team ignores red builds. What do you do first?
The trap

Blaming 'the environment' for all flakiness. Sometimes the test is right and the application genuinely has a race condition — dismissing that as a flaky test hides a production bug.

Link to this question

You have 50 flaky tests and the team now ignores red builds. What do you do?

Flaky Testssenior

The first move is to restore the meaning of the signal, because that's the real damage. A red build that everyone ignores is worse than no build, since it also trains the team to ignore the real…

The first move is to restore the meaning of the signal, because that's the real damage. A red build that everyone ignores is worse than no build, since it also trains the team to ignore the real failures hiding among the noise.

So: quarantine. Move the known-flaky tests into a separate, non-blocking lane immediately. The main pipeline goes green, and green starts meaning something again. This is not sweeping it under the rug — it's making the trustworthy signal usable while you fix the rest.

Then measure and rank. Instrument the suite to record pass/fail history per test, and rank by flake rate. Fix in order of noise contributed; typically a handful of tests generate most of the failures.

Then fix by root cause, not by retry. Group them: sleeps and bad waits, shared state, data collisions, environment. Each family has one systemic fix. If half your flakes are data collisions, the fix is a test-data factory generating unique data, not fifty individual patches.

Then set a policy so it can't recur: a test that flakes more than X times in a rolling window is auto-quarantined and assigned an owner with a deadline. Flake rate goes on the dashboard next to pass rate.

What I would not do is blanket-enable retries. Retries turn a visible problem into an invisible one — the test now passes on attempt two and you never learn that your checkout has a race condition.

Key points
  • Quarantine flaky tests into a non-blocking lane immediately so green means green again
  • Instrument pass/fail history; rank by flake rate — a few tests usually cause most noise
  • Fix by root-cause family (waits, shared state, data collisions), not test by test
  • Set a policy: auto-quarantine after N flakes, with an owner and a deadline; track flake rate as a metric
  • Don't blanket-enable retries — they hide real race conditions in the product
They'll ask next
  • When, if ever, is an automatic retry acceptable?
  • How would you get management to fund two weeks of flake cleanup?
Link to this question

Is automatic retry of failed tests a good idea?

Flaky Testsmidsenior

It's a painkiller. Occasionally justified, frequently abused, and never a cure.

It's a painkiller. Occasionally justified, frequently abused, and never a cure.

The case for: some flakiness genuinely originates outside your control — a shared staging environment, a third-party sandbox that drops one request in a thousand, transient network faults in CI. Failing an entire pipeline because of a blip that will never affect a user is its own kind of waste.

The case against, which is stronger: a retry converts a visible problem into an invisible one. The test passes on attempt two, the build goes green, and nobody ever investigates. If the underlying cause was a real race condition in the application — a double-click that double-charges, say — you have just built a machine for hiding a production defect from yourself. Retries also mask slow degradation: your suite gets flakier month by month and the pass rate never shows it.

Where I land: retries are acceptable only if they are visible and measured. Record every retry, report retry rate as a first-class metric, and treat any test that needed one as a defect to be triaged — not as a pass. If a test needs a retry to be green, it isn't green; it's amber, and someone owns it.

Silent retries are how a team lies to itself.

Key points
  • Retries are a painkiller, not a cure
  • Legitimate for genuinely external transients (shared env, third-party sandbox, network blips)
  • Dangerous because they hide real race conditions — including ones that affect users
  • Only acceptable if visible: record every retry, report retry rate, triage any test that needed one
  • A test that needs a retry to pass isn't green, it's amber
They'll ask next
  • How would you distinguish an environment blip from a genuine app race condition?
  • What retry policy would you set for a suite running on a shared staging box?
Link to this question

How do you keep tests independent of each other?

Framework Designmid

Independence means a test produces the same result whether it runs alone, in a suite, in a different order, or in parallel with fifty others.

Independence means a test produces the same result whether it runs alone, in a suite, in a different order, or in parallel with fifty others. It's the property that makes parallelism possible, and the lack of it is the deepest cause of flakiness.

Concretely, this requires several disciplines.

Each test creates the state it needs, rather than relying on state left by a previous test. If a test needs a registered user, it creates one — ideally through the API, which is fast — rather than assuming test 4 already registered one.

Unique data per test. Emails, usernames, order references generated with a UUID or timestamp so two parallel tests can never collide on the same record.

Clean up, or design so cleanup doesn't matter. Either tear down what you created, or make data unique enough that leftovers are harmless. In some architectures the strongest option is a transactional rollback or a per-test tenant.

No shared mutable fixtures — a session-scoped logged-in browser is fast right up to the day one test logs out and the next twenty fail mysteriously.

No dependence on execution order. If your tests need priority or dependsOnMethods to pass, that's a design smell, not a feature.

The fast way to test all of this: run your suite in a random order, and in parallel. Anything that breaks was never independent — you just hadn't found out yet.

Key points
  • Independence = same result alone, in a suite, in any order, in parallel
  • Each test creates its own state — preferably via API, which is fast
  • Generate unique data (UUID/timestamp) so parallel tests can't collide
  • Avoid shared mutable fixtures; a session-scoped logged-in browser is a trap
  • Prove it by running the suite in random order and in parallel — breakage reveals hidden coupling
They'll ask next
  • How do you handle a test that genuinely needs a long, expensive setup?
  • What's your approach to cleanup when tests fail midway?
Link to this question

How do you handle test data in automation?

Data-Driven Testingmidsenior

Test data is where most automation suites actually break, and it's the topic juniors under-prepare for.

Test data is where most automation suites actually break, and it's the topic juniors under-prepare for.

The strategies, roughly in order of preference:

Generate it per test. A factory creates a fresh entity with unique values (UUID-suffixed email, unique order reference). This gives full isolation and parallel safety, and it's my default.

Seed via API. For state that's expensive to build through the UI — a user with a completed order history, a loyalty account at gold tier — call the API to construct it in one request. Setting up UI-test state through the API is one of the strongest signals of a mature framework, because it's faster, far more stable, and it doesn't test the same UI twice.

Seed via database when no API exists. Faster still, but it couples your tests to the schema, so it's a fallback.

Static/shared fixture data — a small set of reference accounts. Convenient, and dangerous: any test that mutates shared data poisons the others. I'd only use it for genuinely read-only reference data.

What I avoid: depending on production-like data that someone must remember to restore, and hard-coding a user that also exists in someone else's test.

The rule that ties it together: a test that mutates data must own that data.

Key points
  • Default: generate unique data per test via factories — isolation and parallel safety
  • Seed complex state via API, not through the UI — faster, more stable, and a maturity signal
  • DB seeding as a fallback when no API exists; couples you to the schema
  • Static shared data only for read-only reference; any mutation poisons other tests
  • Rule: a test that mutates data must own that data
They'll ask next
  • How do you handle test data in a system where creating a user requires a manual approval step?
  • What do you do about data cleanup when a test fails halfway through?
Link to this question

How do you handle authentication in every test without logging in through the UI each time?

Framework Designmidsenior

Logging in through the UI in every test is one of the most common and most expensive mistakes in a suite.

Logging in through the UI in every test is one of the most common and most expensive mistakes in a suite. It adds seconds to every test, and it makes every single test dependent on the login page — so when login breaks, all four hundred tests fail and the report tells you nothing.

The better approach: authenticate once via the API, then inject the session. Call the login endpoint directly with requests or the equivalent, get the token or session cookie back, and set it into the browser context before navigating. In Playwright this is a stored storageState; in Selenium you add the cookie to the driver before loading the page.

The practical shape: a session-scoped fixture obtains a token per user role (standard user, admin, locked-out user) once; each test then starts already authenticated in the role it needs, in milliseconds instead of seconds.

The things to be careful of: token expiry over a long run, tests that genuinely need to exercise the real login UI (you keep a handful — login itself must be tested through the UI, exactly once), and not letting parallel tests share a mutable session.

The general principle behind it: don't test through the UI what you can set up through the API. Every UI step that isn't the thing under test is pure cost and pure risk.

Key points
  • UI login in every test is slow and makes all tests depend on the login page
  • Authenticate via API, then inject the token/cookie into the browser (Playwright storageState, Selenium add_cookie)
  • Fixture per role: standard, admin, locked-out — obtained once, reused
  • Keep a small number of real UI login tests — login itself still needs UI coverage
  • Principle: never test through the UI what you can set up through the API
They'll ask next
  • How do you handle token expiry during a long suite?
  • What if the app uses OAuth with a third-party identity provider?
Link to this question

How do you run tests across multiple browsers?

Cross-Browser & Gridjuniormid

The mechanism depends on the stack, but the design principle is constant: browser choice must be a runtime parameter, never a code change.

The mechanism depends on the stack, but the design principle is constant: browser choice must be a runtime parameter, never a code change.

In practice that means a config layer where the browser comes from an environment variable or a CLI flag, and a driver/browser factory that instantiates the right one. The tests themselves must contain no browser-specific code — the moment you write if (browser == 'safari') inside a test, the design has failed and it will spread.

For execution at scale you have three options. Selenium Grid (or a Docker Compose Grid) — you run the infrastructure, which is cheap and private but is yours to maintain. Cloud device farms — you rent real browsers and devices, get enormous combination coverage and real mobile hardware, and pay for it. Local parallel execution with headless browsers — fastest for CI, but it can't tell you about real devices.

My usual arrangement: the full cross-browser matrix does not run on every commit. That's slow and mostly redundant. Chrome headless runs on every pull request for fast feedback; the wider matrix runs nightly, or before a release.

And the matrix itself should come from analytics — the browsers your users actually have — not from a list someone copied years ago.

Key points
  • Browser must be a runtime parameter (env var / CLI flag) resolved by a factory — never a code change
  • Zero browser-specific conditionals inside tests
  • Options: self-hosted Grid, cloud device farm, or local headless parallel
  • Don't run the full matrix on every commit: Chrome headless per PR, full matrix nightly/pre-release
  • Derive the matrix from real user analytics
They'll ask next
  • What can't you catch running headless?
  • How do you decide which browsers make the matrix?
Link to this question

What is Selenium Grid and when do you need it?

Cross-Browser & Gridmid

Selenium Grid distributes test execution across multiple machines and browsers. Architecturally there's a hub (in Grid 4, a router/distributor) that receives your test's session request, and nodes…

Selenium Grid distributes test execution across multiple machines and browsers. Architecturally there's a hub (in Grid 4, a router/distributor) that receives your test's session request, and nodes that actually own the browsers. Your test asks for 'Chrome on Linux', the Grid finds a free node that can provide it, and routes the session there.

You need it for two reasons. Parallelism — one machine can only run so many browsers before it starts thrashing and generating flakiness that looks like product bugs but is really CPU starvation. Spread across nodes and your two-hour suite becomes fifteen minutes. Coverage — you need Safari on macOS and you're on a Linux CI box; a Grid node with the right OS solves that.

In modern practice, most teams run Grid as containers via Docker Compose, or use a managed cloud grid rather than maintaining physical machines.

The caveat I'd raise unprompted: Grid gives you parallel capacity, but it doesn't make your tests parallel-safe. If your tests share state or collide on test data, adding a Grid doesn't speed them up — it just makes them fail faster and more confusingly. Test independence is the prerequisite; the Grid is only the engine.

Key points
  • Hub/router receives session requests; nodes own the browsers and execute
  • Use it for parallelism (a single machine thrashes and creates fake flakiness) and OS/browser coverage
  • Usually run as Docker containers today, or rented as a managed cloud grid
  • Grid provides capacity, not safety — parallel-unsafe tests just fail faster
  • Test independence is the prerequisite; the Grid is only the engine
They'll ask next
  • How many browsers can you realistically run on one node?
  • Grid or cloud device farm — how do you choose?
The trap

Thinking Grid makes tests parallel-safe. It provides capacity, not safety. Tests that share state just fail faster and more confusingly when you spread them across nodes.

Link to this question

Playwright vs Selenium — which would you choose and why?

Modern Toolsmidsenior

I'd answer this as an engineer justifying a decision, not by reciting marketing from either camp.

I'd answer this as an engineer justifying a decision, not by reciting marketing from either camp.

Playwright's real advantages. Auto-waiting is built into every action — it waits for the element to be visible, stable, enabled and receiving events before acting, which eliminates the largest single source of Selenium flakiness rather than asking every engineer to remember explicit waits. It drives the browser through the DevTools protocol, so it's faster and can do things Selenium can't easily: intercept and mock network requests, control geolocation and permissions, and manage isolated browser contexts (cheap, fully-isolated sessions, excellent for parallelism). Its tracing — a timeline with DOM snapshots and screenshots per step — makes debugging a CI failure dramatically less painful.

Selenium's real advantages. It's a W3C standard with an enormous ecosystem, twenty years of accumulated answers to obscure problems, and support for browsers and legacy environments Playwright doesn't reach. In many enterprises the existing suite, the existing skills, and the existing Grid are all Selenium — and rewriting a thousand working tests is a poor use of a year.

My decision. New project, modern web app: Playwright. Existing large Selenium suite that works: keep it, and improve the wait strategy. The tool is rarely the bottleneck — framework design is.

Key points
  • Playwright: built-in auto-waiting (kills the main flakiness source), network interception, browser contexts, tracing
  • Selenium: W3C standard, huge ecosystem, broader legacy/browser reach, existing team skills and Grids
  • New modern project → Playwright; large working Selenium suite → keep and improve it
  • The honest close: the tool is rarely the bottleneck, framework design is
They'll ask next
  • What can Selenium do that Playwright can't?
  • How would you migrate a 1,000-test Selenium suite to Playwright — or would you?
The trap

Answering 'it depends' with no position. Interviewers want a defensible choice with stated trade-offs, not a shrug.

Link to this question

What are Cypress's limitations?

Modern Toolsmidsenior

Cypress is genuinely excellent for front-end testing — the developer experience, the time-travel debugger and the automatic waiting are best in class.

Cypress is genuinely excellent for front-end testing — the developer experience, the time-travel debugger and the automatic waiting are best in class. But it's built on a specific architectural decision, and every limitation flows from it: Cypress runs inside the browser, in the same event loop as your application.

That gives it speed and unrivalled introspection, and it costs the following.

Limited multi-tab and multi-window support — because it lives in one browser tab. Flows that open a new tab (OAuth redirects, payment provider pop-ups, 'open in new window' links) are awkward and often require workarounds.

Same-origin constraints. Navigating across different domains inside a single test has historically been restricted; it has improved, but it remains a friction point for cross-domain SSO flows.

Browser support is narrower than Selenium's — notably weaker on Safari/WebKit historically.

No native mobile app testing at all.

Language lock-in: JavaScript/TypeScript only. If your team is a Java or Python shop, that's a real organisational cost.

Parallelisation across machines was historically tied to their paid dashboard, which is a commercial consideration for some teams.

Where it shines: a JS/TS team testing a single-origin web app who want fast, debuggable front-end tests. Where I'd hesitate: complex multi-domain enterprise flows, or a polyglot team.

Key points
  • All limitations flow from one design choice: Cypress runs inside the browser's event loop
  • Weak multi-tab/multi-window support — a problem for OAuth and payment pop-ups
  • Same-origin/cross-domain friction; narrower browser support; no native mobile
  • JavaScript/TypeScript only — an organisational cost for Java or Python teams
  • Best for JS teams on single-origin apps; hesitate for multi-domain enterprise flows
They'll ask next
  • How would you test an OAuth login flow that redirects to a third-party domain?
  • Given those limits, why is Cypress still so popular with developers?
Link to this question

What is BDD, and what's your honest view of Cucumber?

BDDmidsenior

BDD is a collaboration practice, not a testing tool. Its point is that business, development and testing agree on behaviour before code is written, using concrete examples expressed in shared…

BDD is a collaboration practice, not a testing tool. Its point is that business, development and testing agree on behaviour before code is written, using concrete examples expressed in shared language — typically Given/When/Then. The examples become the acceptance criteria and, optionally, the automated tests.

That's the theory, and when a team actually does it, it works: ambiguity gets resolved in a conversation instead of in a defect three weeks later.

My honest view of Cucumber and its cousins: the tool is often adopted without the practice, and then it's pure overhead. The failure pattern is recognisable — the product owner never reads the feature files, the Gherkin is written after the code by a tester, the scenarios are actually UI scripts in disguise (When I click the button with id 'submit'), and you've bought yourself an extra layer of indirection, a step-definition maintenance burden, and regex debugging, in exchange for nothing.

So the question I'd ask before adopting it: will a non-technical stakeholder actually read and shape these scenarios? If yes, BDD earns its cost several times over. If no — if it's engineers writing Gherkin for other engineers — write plain, well-named tests in code and skip the ceremony.

Saying that in an interview signals experience, because everyone who has lived through a decaying Cucumber suite recognises it instantly.

Key points
  • BDD is a collaboration practice (shared examples before coding), not a testing tool
  • Cucumber is frequently adopted without the practice — then it's pure overhead
  • Failure pattern: Gherkin written after the code, by testers, describing UI clicks, read by nobody
  • The deciding question: will a non-technical stakeholder actually read and shape these scenarios?
  • If not, write well-named tests in code and skip the ceremony
They'll ask next
  • Write a well-formed Gherkin scenario and then a badly-formed one, and explain the difference.
  • How do you keep step definitions from becoming a maintenance nightmare?
Link to this question

How do you handle dropdowns, alerts, iframes and multiple windows in Selenium?

Selenium Corejunior

Each is a distinct mechanism, and knowing them is basic competence.

Each is a distinct mechanism, and knowing them is basic competence.

Native <select> dropdowns: use the Select class — selectByVisibleText, selectByValue, selectByIndex. Important caveat: the Select class only works on genuine <select> elements. Most modern UIs build custom dropdowns from divs and spans, and for those you interact with the elements directly like any other click target. Candidates who only know Select get stuck immediately on a React app.

JavaScript alerts / confirms / prompts: these are browser-level, not DOM elements, so you can't locate them. Switch to them — driver.switchTo().alert() — then accept(), dismiss(), getText(), or sendKeys() for a prompt. Trying to find an alert with a locator is a classic junior mistake.

Iframes: an iframe is a separate document. Selenium cannot see inside it until you switch context — driver.switchTo().frame(...) by index, name/id, or WebElement — and you must switchTo().defaultContent() to get back out. Forgetting to switch back is the source of a lot of baffling 'element not found' failures.

Multiple windows/tabs: capture getWindowHandle() (current) and getWindowHandles() (all), then switchTo().window(handle) to move. Always store the original handle so you can return to it.

The common thread: for alerts, frames and windows, you must change context — the driver only ever acts on the context it's currently in.

Key points
  • Select class works only on real <select> tags — custom div dropdowns are just clicks
  • Alerts are browser-level: switchTo().alert(), then accept/dismiss/getText/sendKeys
  • Iframes are separate documents: switchTo().frame(), and remember defaultContent() to return
  • Windows: store getWindowHandle(), enumerate getWindowHandles(), switchTo().window()
  • Common thread: the driver only acts on its current context — you must switch
They'll ask next
  • How would you handle a dropdown built from divs in a React app?
  • What happens if you forget to switch back from an iframe?
Link to this question

How do you handle file uploads and downloads in automated tests?

Selenium Coremid

Uploads are usually simpler than people expect. If the page uses a standard <input type="file">, you don't click it and drive the OS dialog — Selenium can't control OS dialogs anyway.

Uploads are usually simpler than people expect. If the page uses a standard <input type="file">, you don't click it and drive the OS dialog — Selenium can't control OS dialogs anyway. You send the absolute file path directly to the input element with sendKeys("/path/to/file.pdf"). The browser handles the rest.

The complications: the input is often hidden behind a styled button, so you may need to locate the hidden input directly rather than the visible one. And drag-and-drop upload zones don't use a file input at all, which usually means either finding the underlying input anyway or dispatching a JavaScript event. In Playwright this is cleaner — setInputFiles handles it, and there's an explicit file-chooser API.

Downloads are the harder half, because once the file leaves the browser it's outside the automation's world. The approach: configure the browser profile at startup to download automatically to a known directory without prompting (Chrome options / Firefox preferences). Then in the test, wait for the file to appear in that directory — polling for existence and for the file to stop growing, since a partially-written file will fail your assertions — and then assert on it: name, size, content, and for a PDF or CSV, parse and check the actual data.

In CI, remember the container needs that directory to exist and be writable.

Key points
  • Upload: sendKeys the absolute path to the <input type=file> — never drive the OS dialog
  • Watch for hidden inputs behind styled buttons, and drag-drop zones with no input
  • Download: configure the browser profile to auto-download to a known directory with no prompt
  • Poll for the file to exist AND stop growing (partial files break assertions), then assert on content
  • In CI, ensure the download directory exists and is writable in the container
They'll ask next
  • How would you verify the contents of a downloaded PDF?
  • How do you handle a download in a headless CI container?
Link to this question

How do you capture screenshots and useful diagnostics when a test fails?

Reportingjuniormid

Automatically, never manually — because the only failure that matters is the one that happens at 2am in CI when nobody is watching.

Automatically, never manually — because the only failure that matters is the one that happens at 2am in CI when nobody is watching.

The hook belongs in the framework, not in individual tests: a pytest hook (pytest_runtest_makereport), a TestNG ITestListener, a JUnit extension. On failure, it captures everything and attaches it to the report.

What I capture, in rough order of usefulness:

A screenshot at the moment of failure — cheap and immediately orienting.

The page HTML source, because a screenshot won't tell you that the element you wanted was present but hidden, or that an error message existed in the DOM behind a modal.

Browser console logs and failed network requests — this is the one juniors omit, and it's often where the real answer is. A test that 'couldn't find the confirmation message' is much easier to diagnose when the console shows a 500 from the orders API.

The current URL, which instantly reveals unexpected redirects to a login or error page.

A trace or video if the tool supports it — Playwright's trace viewer, with DOM snapshots per step, turns a twenty-minute investigation into a two-minute one.

All of it attaches to the report (Allure, or the CI artifacts) so the person triaging never has to reproduce locally just to see what happened.

Key points
  • Automate capture in a framework hook/listener — never rely on manual screenshots
  • Screenshot + page source (source reveals hidden or covered elements a screenshot can't)
  • Console logs and failed network requests — most often where the real cause is
  • Current URL, to catch unexpected redirects
  • Traces/video where available; attach everything to the report so triage needs no local repro
They'll ask next
  • What's in a failure report that would let you diagnose without reproducing locally?
  • How do you avoid your artifacts eating all your CI storage?
Link to this question

How do you integrate your automated tests into CI?

CI Basicsmid

The principle is that tests must run on a trigger, not on a human remembering. A suite that runs when someone feels like it provides no safety.

The principle is that tests must run on a trigger, not on a human remembering. A suite that runs when someone feels like it provides no safety.

A typical arrangement, in tiers by speed and cost:

On every pull request: linting, unit tests, and a fast API smoke suite. This must finish in minutes — if the PR gate takes forty minutes, developers start merging around it, and your gate is now theatre.

On merge to main: a broader regression run, sharded across parallel workers to keep the wall-clock time acceptable.

Nightly: the full cross-browser matrix, longer end-to-end journeys, and anything slow or expensive.

Pre-release: the complete suite, plus performance checks.

Mechanically that's a pipeline definition (GitHub Actions workflow, Jenkinsfile) that checks out the code, installs dependencies, spins up the environment — usually browsers in containers — runs the tests with the environment supplied as variables, and publishes results and artifacts.

Details that separate someone who has actually done it: secrets come from the CI secret store, never the repo. Artifacts (reports, screenshots, traces) are published on failure so triage doesn't require a local repro. The exit code must actually fail the build — a surprising number of pipelines run tests and ignore the result. And results should be visible where the team already looks: a comment on the PR, or a Slack message.

Key points
  • Tests run on triggers, not on someone remembering
  • Tier by speed: PR = lint + unit + API smoke (minutes); merge = sharded regression; nightly = full matrix; pre-release = everything
  • A slow PR gate gets bypassed — speed is a correctness property of the gate
  • Secrets from the CI store; artifacts published on failure; exit code must actually fail the build
  • Surface results where the team already looks (PR comment, Slack)
They'll ask next
  • Your PR pipeline now takes 40 minutes. What do you cut?
  • How do you handle tests that need a database or a third-party service in CI?
The trap

Describing CI as 'we run tests after merging'. If the gate is slow, developers route around it, and a gate everyone bypasses is theatre. Speed is a correctness property of a pipeline.

Link to this question

What Git workflow do you use, and how do you resolve a merge conflict?

Version Controljuniormid

Typically a feature-branch workflow: branch from main, commit in small logical units with messages that explain why rather than restating the diff, push, open a pull request, get a review, and merge…

Typically a feature-branch workflow: branch from main, commit in small logical units with messages that explain why rather than restating the diff, push, open a pull request, get a review, and merge once CI is green. Some teams use trunk-based development with very short-lived branches and feature flags — which pairs well with strong automated tests, because the tests are what make merging into main constantly safe.

A merge conflict happens when two branches change the same lines and Git can't decide which version wins. Resolving it means: pull the latest main into your branch (merge or rebase), open the conflicted files, and look for the markers — <<<<<<<, =======, >>>>>>> — delimiting your change and theirs.

The part that matters is what you do next. You don't blindly take yours, and you don't blindly take theirs. You read both, understand what each was trying to achieve, and construct the version that preserves both intentions — which is sometimes neither of the two literal options. Then you remove the markers, run the tests, and commit.

If the conflict is in code you don't understand, talk to whoever wrote the other side. Thirty seconds of conversation beats silently deleting someone's work — which, in a test suite, means silently deleting coverage.

Key points
  • Feature branches + PR + review + green CI before merge; trunk-based works when tests are strong
  • Conflict = both branches changed the same lines; markers <<<<<<< ======= >>>>>>> show each side
  • Resolve by understanding both intentions — the right answer is sometimes neither literal option
  • Always run the tests after resolving, before committing
  • If you don't understand the other side, ask the author — silently discarding their change deletes coverage
They'll ask next
  • What's the difference between merge and rebase, and when do you use each?
  • You've committed a secret to the repo. What do you do?
Link to this question

What do you look for when reviewing someone else's test code?

Framework Designsenior

I review test code with the same rigour as production code, because it is production code — it gates every release, and when it's bad it costs the team daily.

I review test code with the same rigour as production code, because it is production code — it gates every release, and when it's bad it costs the team daily.

Can this test fail? The first question, always. An assertion that can never be false is worse than no test: it inflates the coverage number while protecting nothing.

Is the failure message useful? When this breaks at 3am, will the message alone tell the on-call engineer what's wrong, or will they have to reverse-engineer it?

Is it independent? Does it create its own data, or is it quietly relying on state from another test or a shared fixture? Would it survive being run in parallel, or in a random order?

Any sleeps? Any hard-coded waits are a rejection, with a request for a conditional wait.

Are locators durable? Positional XPaths and styling-based classes get flagged.

Does it test one thing? A test named testCheckout that asserts twenty unrelated facts tells you nothing when it goes red.

Is it at the right layer? A pure business rule being verified through six clicks in a browser should be an API test.

Is it readable? Could a new joiner understand the scenario without reading the framework?

The review comment I write most often is simply: what would make this fail?

Key points
  • Test code IS production code — it gates releases
  • First question: can this test actually fail? Assertions that can't fail are worse than nothing
  • Independence: own data, no reliance on other tests, safe in parallel and random order
  • Reject sleeps; flag positional/style-based locators
  • One logical thing per test; correct layer (business rules belong in API tests, not six UI clicks)
They'll ask next
  • How do you give that feedback to a defensive junior without demoralising them?
  • Should developers review test code, or only QA?
Link to this question

How do you decide which layer to automate a given check at?

Automation Strategysenior

My rule: automate at the lowest layer that can genuinely catch the bug.

My rule: automate at the lowest layer that can genuinely catch the bug.

The reasoning is cost. As you go up the stack — unit, integration/API, UI — tests get slower, more brittle, more expensive to maintain, and slower to diagnose when they fail. A unit test fails and points at a function. A UI test fails and you don't know whether the bug is in the front end, the API, the database, the environment, or the test.

So: a calculation or validation rule belongs in a unit test. A contract between services — status codes, schema, auth, error handling — belongs at the API layer. The UI layer should be reserved for things that genuinely only exist in the browser: does the user journey wire together, does the form submit, does the error render where a user can see it.

The common anti-pattern is testing business logic through the UI. Thirty UI tests covering thirty discount permutations is a slow, flaky, expensive way to test something that one parametrised API test could cover in two seconds — and the UI tests will break every time a designer moves a button, telling you nothing about discounts.

The practical question I ask of every proposed UI test: is the browser essential to what I'm verifying? If not, push it down.

Key points
  • Rule: automate at the lowest layer that can catch the bug
  • Going up the stack costs speed, stability, maintenance, and diagnosability
  • Calculations → unit; contracts, auth, error handling → API; journey wiring and rendering → UI
  • Anti-pattern: thirty UI tests for thirty discount permutations that one API test could cover
  • The test question: is the browser essential to what I'm verifying? If not, push it down
They'll ask next
  • Give me an example of something that genuinely can only be tested through the UI.
  • How do you convince a team to delete UI tests?
Link to this question

How do you test that an element is NOT present, without slowing your suite down?

Waits & Synchronisationmid

This is a genuinely tricky case, and a good discriminator, because the naive implementation quietly destroys your suite's runtime.

This is a genuinely tricky case, and a good discriminator, because the naive implementation quietly destroys your suite's runtime.

The naive approach is to look for the element and treat the resulting NoSuchElementException as success. The problem: if you have an implicit or explicit wait configured, that lookup will wait the full timeout every time before throwing. A ten-second wait on fifty 'should not be present' assertions is over eight minutes of your suite spent waiting for things that were never going to appear.

The better approaches:

Use a proper conditioninvisibilityOfElementLocated or an equivalent — but with a deliberately short timeout, because you're asserting an absence you expect immediately, not waiting for something to arrive.

Query the collection instead: findElements (plural) returns an empty list rather than throwing, so assertTrue(driver.findElements(locator).isEmpty()) returns immediately.

Wait for the positive condition first, then assert the negative. This is the most robust pattern, because it removes the race entirely: wait for the page state that should exist ('the error banner is gone and the success message is shown'), and only then assert that the thing which shouldn't be there isn't. Otherwise you risk asserting absence before the element has had a chance to appear, which passes for the wrong reason — a false negative that hides a real bug.

That last risk is the real trap in this question.

Key points
  • Naive 'catch NoSuchElementException' waits the full timeout on every negative assertion — a huge hidden cost
  • Use findElements (plural) and assert the list is empty — returns immediately
  • Or use an invisibility condition with a deliberately short timeout
  • Most robust: wait for the positive state first, then assert the absence
  • The real trap: asserting absence too early passes for the wrong reason and hides a genuine bug
They'll ask next
  • How would you assert that an error message never appears at all during a flow?
  • What's the risk of a very short timeout on a negative assertion?
Link to this question

What is the Page Factory, and would you use it?

Page Object Modelmid

Page Factory is a Selenium (Java) helper for implementing page objects. You annotate fields with @FindBy(id = "login") and call PageFactory.initElements(driver, this) in the constructor.

Page Factory is a Selenium (Java) helper for implementing page objects. You annotate fields with @FindBy(id = "login") and call PageFactory.initElements(driver, this) in the constructor. It uses reflection to populate the fields with lazily-initialised proxy objects — the element isn't actually located until you first use it.

It reduces boilerplate and it reads nicely.

My honest view: I don't reach for it any more, and I'd say so directly.

The reasons. The lazy proxy makes stale element behaviour subtle and harder to reason about, especially in single-page applications that re-render constantly — the exact environment most of us now work in. @FindBy is a Java-only, Selenium-only construct, so the pattern doesn't transfer if the team moves to Playwright or works in Python. It couples your page object to the framework's reflection magic, which makes it harder to unit-test or reason about. And the boilerplate it saves is genuinely small — a well-written By locator as a private field costs one line.

More importantly, the idea people care about is the Page Object Model itself: encapsulating locators and interactions. Page Factory is just one implementation of it, and it's not the one I'd choose today. Knowing the difference between a pattern and one dated implementation of it is what this question is really testing.

Key points
  • @FindBy annotations + PageFactory.initElements; reflection creates lazily-initialised element proxies
  • Reduces boilerplate, reads nicely
  • Downside: lazy proxies make stale-element behaviour subtle in modern SPAs
  • Java- and Selenium-specific — doesn't transfer to Playwright or Python
  • POM is the pattern; Page Factory is one dated implementation of it. Don't confuse the two
They'll ask next
  • So how do you structure a page object without it?
  • What's the difference between the pattern and the implementation here?
Link to this question

How would you automate testing of an application that has no test IDs and a constantly changing DOM?

Locatorssenior

First I'd challenge the premise, respectfully, because accepting it is how teams end up maintaining an unmaintainable suite forever.

First I'd challenge the premise, respectfully, because accepting it is how teams end up maintaining an unmaintainable suite forever. A DOM with no stable hooks is a testability defect, and testability is a legitimate non-functional requirement. I'd make the business case to the engineering lead: adding data-testid costs developers minutes and saves QA weeks per year in rework. Getting that agreed is the highest-value thing I can do, and it's a collaboration problem, not a tooling one.

While that's in progress, I'd stabilise what I can:

Anchor to semantic attributesrole, aria-label, name, placeholder, visible text. These are more stable than styling because changing them changes behaviour and accessibility, not just appearance. Playwright's getByRole and getByLabel are built on exactly this insight.

Anchor to stable ancestors and traverse relatively, rather than encoding a full path.

Push coverage down a layer: if the UI is genuinely hostile, test the business logic at the API layer, and keep only a thin skin of UI tests over the journeys that must be verified in a browser. That converts an unmaintainable problem into a manageable one.

And I'd keep a record of the maintenance cost — hours per sprint spent fixing locators. That number, presented to management, is what eventually funds the test IDs.

Key points
  • Challenge the premise: untestable DOM is a testability defect, and testability is a real NFR
  • Make the business case with numbers — minutes for devs, weeks saved for QA
  • Meanwhile: anchor to semantic attributes (role, aria-label, name, text) — stable because they carry meaning
  • Anchor to stable ancestors and traverse; avoid encoding full paths
  • Push coverage down to the API layer and keep only a thin UI skin
  • Track locator-maintenance hours — that metric is what funds the fix
They'll ask next
  • How do you quantify 'weeks of rework' credibly to a sceptical manager?
  • Where do self-healing locator tools fit here — and what's the risk?
Link to this question

What is a headless browser, and what are the trade-offs?

Cross-Browser & Gridjuniormid

A headless browser runs the full browser engine without rendering a visible window. It parses HTML, executes JavaScript, applies CSS and builds the layout — it simply doesn't paint pixels to a screen.

A headless browser runs the full browser engine without rendering a visible window. It parses HTML, executes JavaScript, applies CSS and builds the layout — it simply doesn't paint pixels to a screen.

Advantages: it's faster (no rendering and compositing overhead), it uses far less memory and CPU, and — crucially — it runs in environments with no display server at all, which is exactly what a CI container is. That last point is the main reason it exists in practice.

Trade-offs, and this is what the question is really after. Debugging is harder: you can't watch what happened, so you're dependent on screenshots, video and traces (which is one reason those matter so much). Some rendering-dependent issues genuinely won't surface — a CSS bug that only appears at a particular viewport, a font that fails to load, an element that's technically present but visually covered. Historically, headless mode also behaved subtly differently from headed mode — different default window sizes, different user agents, occasionally different behaviour that caused the maddening 'passes headless, fails headed' class of bug.

My practical approach: run headless in CI for speed, run headed locally when debugging, and don't rely on headless alone for anything genuinely visual — that's what visual regression tooling and real-device checks are for.

Key points
  • Full browser engine, no painted window — parses, executes JS, lays out, but doesn't render to screen
  • Faster, lighter, and runs in CI containers with no display server (the real reason it's used)
  • Trade-off: harder to debug, and rendering-dependent issues can be missed
  • Historic gotcha: subtle headless-vs-headed differences (window size, user agent) cause confusing failures
  • Headless in CI, headed locally for debugging, and don't trust headless alone for visual correctness
They'll ask next
  • You have a test that passes headless and fails headed. How do you investigate?
  • What kinds of defect can headless never catch?
Link to this question

How do you structure assertions and reporting so that a failure is diagnosable without re-running the test?

Reportingsenior

The design goal I hold is: the report alone should be enough. If an engineer has to check out the branch and reproduce locally just to understand what failed, the framework has failed them — and in…

The design goal I hold is: the report alone should be enough. If an engineer has to check out the branch and reproduce locally just to understand what failed, the framework has failed them — and in practice that's when teams start ignoring red builds, because triage is too expensive.

That means three things.

Assertion messages carry context. Not assertTrue(isVisible) but assertEquals(expectedTotal, actualTotal, "Order total after applying PROMO10 for a gold-tier customer"). The message should let someone who has never seen this test understand the intent.

Failures capture the full state automatically, via a framework-level hook: screenshot, DOM source, browser console log, failed network requests with their status codes and payloads, current URL, the test data used (which is invaluable for parametrised tests — 'which of the 40 datasets failed?'), and a trace where the tool supports it.

The report structures it. Allure or an equivalent, with the failure at the top, steps leading to it, and attachments inline. Grouped by suite, linked from the CI run, retained long enough to spot trends.

And one thing that's easy to miss: test names must be self-describing. test_checkout_fails_when_promo_code_expired tells you what broke from the CI summary line alone. test_checkout_3 requires opening the code. Over a thousand tests, that difference is hours per week.

Key points
  • Design goal: the report alone is enough — no local reproduction needed to triage
  • Assertion messages must carry intent and context, not just a boolean
  • Auto-capture on failure: screenshot, DOM, console logs, failed network calls, URL, the dataset used, trace
  • Structured report (Allure) linked from CI, retained long enough to see trends
  • Self-describing test names — the CI summary line should already tell you what broke
They'll ask next
  • What's the first thing you look at when a CI-only failure appears?
  • How long do you retain artifacts, and why?
Link to this question

A test passes locally but fails in CI. How do you investigate?

Flaky Testsmidsenior

I treat it as a difference-finding exercise. The test is deterministic in principle; something in the two environments differs, and my job is to identify the variable.

I treat it as a difference-finding exercise. The test is deterministic in principle; something in the two environments differs, and my job is to identify the variable.

I'd work through the usual suspects in order of likelihood.

Timing. CI machines are typically slower and more contended than a laptop, and they're often running many jobs in parallel. Waits that are 'just enough' locally fail under load. This is the most common cause by a wide margin.

Headless versus headed — different viewport size, different user agent, elements outside the visible area, hover states that don't exist.

Test data and state. Locally you're on a database you've been using all week, with data from previous runs. In CI it's fresh — or worse, shared with other parallel jobs colliding on the same records.

Environment configuration: different base URL, missing environment variable, an unset feature flag, a secret that isn't injected.

Parallelism. It passes alone locally, but in CI it runs alongside fifty siblings competing for the same test user.

Time and locale — the CI container is on UTC, your laptop isn't. Tests near midnight, or asserting on formatted dates, break exactly this way.

My first move is always to look at the artifacts — screenshot, console, network, video — before changing a single line. And then I try to reproduce CI conditions locally: run headless, run in a container, run the full suite in parallel rather than the single test. Guessing is slower than reproducing.

Key points
  • Frame it as difference-finding: something differs between the two environments
  • Most common: timing — CI is slower and contended, so marginal waits fail
  • Also: headless vs headed, fresh vs stale data, missing env vars/flags, parallel collisions, UTC vs local timezone
  • Look at artifacts (screenshot, console, network, video) before changing any code
  • Reproduce CI conditions locally — headless, containerised, full suite in parallel — rather than guessing
They'll ask next
  • You reproduce it locally in parallel mode. What does that tell you?
  • How would you prevent this class of problem in the first place?
Link to this question

How do you test a feature that depends on a third-party service you can't control?

Framework Designmidsenior

The core insight is to separate what you're actually testing from what you're merely depending on. If I'm testing my checkout logic, the payment provider is a dependency, not the subject.

The core insight is to separate what you're actually testing from what you're merely depending on. If I'm testing my checkout logic, the payment provider is a dependency, not the subject. Letting their sandbox's uptime determine whether my test suite is green is a bad trade.

So, layered:

Mock or stub it for the majority of tests. Intercept the call and return controlled responses. This makes the tests fast, deterministic, and — crucially — lets me test the scenarios their sandbox can't easily produce on demand: a declined card, a timeout, a malformed response, a 500, a slow response that trips my retry logic. Those failure paths are where the real bugs live, and they're precisely the ones you cannot reliably trigger against a live third party.

Keep a small number of real integration tests against their actual sandbox, run less frequently — nightly, or pre-release. These exist to catch the thing mocks structurally cannot: the provider changing their contract. A mock will happily keep passing forever against an API shape that no longer exists.

Consider contract testing if they publish a spec, so a breaking change is caught explicitly rather than through a mysterious failure.

That combination — mocked for speed and coverage of failure paths, a thin real layer for contract drift — is the standard mature answer, and stating the reason for each half is what earns the marks.

Key points
  • Separate what you're testing from what you merely depend on
  • Mock/stub for most tests: fast, deterministic, and lets you trigger declines, timeouts, 500s and malformed responses on demand
  • Failure paths are where the bugs are — and are exactly what a live sandbox won't reliably produce
  • Keep a few real integration tests, run nightly, to catch contract drift a mock can never see
  • Contract testing where a published spec exists
They'll ask next
  • What's the risk of mocking too much?
  • How would you detect that the third party changed their API before your users do?
Link to this question

What is visual regression testing and when is it worth it?

Modern Toolsmidsenior

Visual regression testing captures a screenshot of a page or component and compares it against an approved baseline image, flagging pixel differences for a human to approve or reject.

Visual regression testing captures a screenshot of a page or component and compares it against an approved baseline image, flagging pixel differences for a human to approve or reject.

It catches a class of defect that functional automation is structurally blind to. A Selenium test asserts the checkout button exists and is clickable; it passes happily while a CSS change has rendered the button white-on-white, or pushed it off-screen, or collapsed the layout on mobile. Functionally correct, visually broken, and completely invisible to your suite.

Where it's worth it: design systems and component libraries (the highest value — one component regression can break every page), landing and marketing pages where appearance is the product, and responsive layouts across breakpoints.

Where it hurts: pages with genuinely dynamic content. Timestamps, live data, ads, animations, and randomised content produce endless false positives, and a tool that cries wolf gets muted. Mitigations exist — masking dynamic regions, freezing time, disabling animations, using tolerance thresholds — but they need real investment.

The honest limitation, and the thing to say out loud: it tells you that something changed, not that something is wrong. Every diff still needs a human to judge intent, so the maintenance cost is baked in. That's why I'd apply it surgically to components and key pages rather than blanket-screenshotting an entire application.

Key points
  • Screenshot compared against an approved baseline; pixel diffs surfaced for human review
  • Catches what functional tests can't: white-on-white buttons, broken layouts, collapsed responsive views
  • Best value on design systems/component libraries and appearance-critical pages
  • Dynamic content (timestamps, ads, animations) causes false positives — mask, freeze time, disable animations
  • It reports change, not wrongness — a human still judges intent, so apply it surgically
They'll ask next
  • How do you handle a baseline that legitimately needs to change every sprint?
  • Would you gate a release on a visual diff? Why or why not?
The trap

Selling visual regression as a way to catch bugs automatically. It reports that something CHANGED, not that something is WRONG. A human still judges every diff.

Link to this question

How would you approach automating an application you've never seen before?

Automation Strategymidsenior

I wouldn't start by writing tests. I'd start by understanding risk and testability, because automation written before you understand the system is automation you throw away.

I wouldn't start by writing tests. I'd start by understanding risk and testability, because automation written before you understand the system is automation you throw away.

First, learn the product. What does it do, who uses it, what are the journeys that make money or lose data if they break? Use the app as a user for a day. Talk to support if they exist — they know what breaks.

Then assess testability. Are there stable hooks in the DOM? Is there an API, and is it documented? How is test data created? Can I reset state? Are there environments I can safely hammer? The answers determine what's even feasible, and any gap here is a conversation to have with engineering now, not in three months.

Then check what already exists — unit tests, an old suite, a CI pipeline, and what the escaped-defect history says about where the product actually breaks. Real defect history beats intuition about what to cover.

Then start small and prove value fast. A smoke suite over the two or three critical journeys, running in CI, green and trusted. That earns the credibility to invest further.

Then expand by risk, layering downward — push checks to the API where possible rather than building a UI monolith.

The mistake I'd avoid is what a lot of people do: opening the IDE on day one and automating whatever screen they saw first.

Key points
  • Don't write tests first — understand risk and testability first
  • Learn the product and its money/data-critical journeys; talk to support
  • Assess testability: DOM hooks, API availability, test data creation, state reset, environments
  • Look at escaped-defect history — real data beats intuition about where to cover
  • Start with a small trusted smoke suite in CI to earn credibility, then expand by risk, pushing checks down to the API
They'll ask next
  • What would you deliver in your first two weeks?
  • You find there's no API and no test IDs. Does your plan change?
Link to this question

How do you keep an automation suite maintainable as it grows to thousands of tests?

Framework Designsenior

Suites don't collapse from a single bad decision; they rot from a hundred small ones. Keeping one healthy at scale is an ongoing engineering discipline, not a setup task.

Suites don't collapse from a single bad decision; they rot from a hundred small ones. Keeping one healthy at scale is an ongoing engineering discipline, not a setup task.

Ruthless layering. Locators in one place, interactions in another, assertions only in tests. The moment a locator appears inside a test file, the erosion has begun.

Shape the pyramid deliberately. The single biggest cause of unmaintainable suites is too many UI tests. Continuously push checks down to API and unit level. A suite of 3,000 UI tests is not an achievement; it's a liability.

Delete tests. This is the one people find hardest. A test that has never failed in two years and duplicates coverage elsewhere is costing you runtime and maintenance for nothing. Coverage is not a ratchet that may only increase.

Enforce quality in review — no sleeps, no shared state, durable locators, meaningful assertions — and back it with linting where possible.

Watch the suite's own metrics: runtime trend, flake rate per test, and which tests have ever actually caught a defect. That last one is uncomfortable and revealing.

Keep the feedback loop fast. If the suite takes two hours, people stop running it, and an unrun suite protects nothing. Shard it, parallelise it, and tier it by trigger.

The underlying stance: the suite is a product, it has users (the developers), and it needs an owner who is accountable for its health.

Key points
  • Ruthless layering — a locator inside a test file is the first sign of rot
  • Continuously push checks down the pyramid; 3,000 UI tests is a liability, not an achievement
  • Delete tests — coverage is not a one-way ratchet
  • Enforce standards in code review; automate the enforcement where you can
  • Track the suite's own metrics: runtime trend, flake rate, and which tests ever caught a real defect
  • Treat the suite as a product with users (developers) and an accountable owner
They'll ask next
  • How do you decide a test is safe to delete?
  • Your suite takes 2 hours. Walk me through getting it to 20 minutes.
Link to this question

What's the difference between a test that's failing and a test that's broken?

Flaky Testsmidsenior

A failing test is doing its job: the product changed in a way that violates an expectation, and the test told you. That's a success, even though it's red. Someone should look at the product.

A failing test is doing its job: the product changed in a way that violates an expectation, and the test told you. That's a success, even though it's red. Someone should look at the product.

A broken test is a defect in the test itself: a locator that no longer matches after a legitimate refactor, a wait that's too short for a slower environment, hard-coded data that expired, an assertion on a value that was always going to drift, like today's date. The product may be perfectly fine. Someone should look at the test.

The distinction matters enormously in practice, because the triage path is completely different and because conflating the two is how suites die. If every red is assumed to be a broken test, real regressions get quietly patched away — someone 'fixes' the test by loosening the assertion until it passes, and the bug ships. If every red is assumed to be a real bug, engineers waste days chasing phantoms and start ignoring the suite.

The practical discipline: when a test goes red, the first question is did the product's behaviour actually change? Verify manually or against the API before touching the test. And the answer must be recorded, because the ratio of genuine failures to broken tests over time is one of the truest measures of your suite's health — a suite whose reds are 90% broken tests is not protecting anyone.

Key points
  • Failing = the product violated an expectation. The test worked. Investigate the product.
  • Broken = a defect in the test (stale locator, short wait, expired data). Investigate the test.
  • Conflating them kills suites: 'fixing' a test by loosening an assertion ships the bug
  • First triage question: did the product's behaviour actually change? Verify before editing the test
  • The ratio of genuine failures to broken tests is one of the truest measures of suite health
They'll ask next
  • How do you stop engineers from 'fixing' a test by weakening its assertion?
  • What would that ratio look like in a healthy suite?
Link to this question