SDET Interview Questions · 2026

SDET Interview Questions (2026): Most Asked, With Answers

A candidate at a mid-size logistics company got asked last year to explain why a Selenium test kept failing intermittently on a button that clicked fine every single time a human tried it by hand. He'd used Thread.sleep(3000) everywhere in his framework for two years and never once needed a real answer. The interviewer wasn't testing whether he knew Selenium syntax. She was testing whether he understood that a hardcoded sleep and a wait condition solve two completely different problems, and that only one of them survives contact with a slow CI runner. Selenium's own documentation is blunt about the related mistake candidates make constantly: "Do not mix implicit and explicit waits. Doing so can cause unpredictable wait times" (Selenium WebDriver documentation).

SDET (Software Development Engineer in Test) interviews sit in an odd spot between a QA interview and a backend engineering interview. You're expected to write real code, reason about a distributed system's failure modes, and still be fluent in test theory that a lot of pure developers never learn on the job, severity versus priority, the shape of the test pyramid, why a mock and a stub aren't interchangeable words for the same thing. Most prep guides split neatly into "manual testing trivia" and "coding problems," which misses the part that actually decides senior loops: whether you can explain why a test is flaky and fix the root cause instead of adding a retry and moving on.

This page covers SDET interview questions across eight areas: core testing fundamentals and the test pyramid, Selenium and UI automation, API and service-layer testing, framework design and test patterns, CI/CD and pipeline integration, performance and non-functional testing, coding and data structure questions specific to test tooling, and the harder architecture questions that come up once a team scales past a handful of engineers.

50Questions
Test PyramidCore Model
Selenium WebDriverCommon Tool
YesCoding Round

SDET fundamentals: what the role actually covers

Every SDET loop opens somewhere near here. It's a warm-up, but a candidate who can't cleanly separate these ideas usually struggles later when the questions get concrete.

Easy questions

15

An SDET writes production-quality code, but the code's job is building and maintaining the test infrastructure rather than shipping the feature itself: automation frameworks, custom test harnesses, CI integrations, internal tooling that other engineers and testers rely on. A manual tester executes test cases largely by hand and doesn't necessarily write code. A pure software engineer builds the product and may write unit tests for their own code, but doesn't usually own the automation strategy across the whole team.

The practical difference shows up in what gets reviewed. A manual tester's output is a test case document or a bug report. An SDET's output is a pull request, reviewed the same way any other code change is, with the added expectation that the tests it adds are themselves reliable and fast enough that nobody starts ignoring them.

The test pyramid describes three layers by granularity: a wide base of fast unit tests, a narrower middle layer of service or integration tests that check how components talk to each other, and a thin top layer of full end-to-end tests that exercise the whole system through the UI. The shape matters because each layer up trades speed and isolation for realism. End-to-end tests catch things unit tests can't, but they're slow, they're flakier, and a failure doesn't tell you which component broke.

Teams that invert the pyramid, heavy on UI tests and light on unit tests, end up with what's sometimes called an ice-cream cone: a slow, expensive test suite that takes twenty minutes to run and still can't tell you where a regression came from. I'd rather see a candidate say "I write ten times more unit tests than UI tests, on purpose" than list the layer names from memory.

Severity measures how badly a defect breaks the system, does it crash the app, corrupt data, or just misalign a button. Priority measures how urgently it needs fixing relative to everything else in the backlog, which depends on business context, not just technical damage.

A classic disagreement: a typo in the company's own logo alt text on the homepage is low severity, nothing breaks, but high priority, because it's visible to every visitor and embarrassing enough to fix before the next release. Meanwhile a crash in a rarely-used admin export feature is high severity but might sit at lower priority if only two internal users ever touch that screen.

Black box testing checks behavior against requirements without looking at the code, you only see inputs and outputs. White box testing uses knowledge of the internal implementation, branches, loops, code paths, to design tests that hit specific logic. Gray box sits in between: you have partial knowledge of the internals, enough to design smarter tests, but you're still largely validating behavior rather than tracing every line.

An SDET typically works in white and gray box territory more than a manual tester does, since writing a unit test around a specific conditional branch requires reading the code that branch lives in.

Alpha testing happens inside the company, before the product goes out the door, usually run by internal QA or dogfooding employees in an environment that mirrors production. Beta testing happens after that, with a limited set of real external users running the product in their own environment, catching the class of bugs that only shows up on hardware, networks, or usage patterns a company's internal testers don't have.

A useful bug report gives someone else everything they need to reproduce the failure without asking you a follow-up question: clear reproduction steps in order, the expected result versus the actual result, environment details (browser, OS, build version), and evidence, a screenshot, a log snippet, a failing request ID.

The detail people skip most often is the expected result. "The button doesn't work" tells the reader nothing about what should have happened instead. Writing that line first, before describing what actually happened, forces you to confirm you actually know what correct behavior looks like.

A test scenario is a high-level statement of what to test, "verify a user can check out with a saved payment method." A test case is the detailed, step-by-step breakdown of exactly how to test that scenario, specific input values, exact click order, and the precise expected result at each step. One scenario usually expands into several concrete test cases covering different inputs and edge cases.

Smoke testing is a quick, broad check that the build isn't fundamentally broken, does the app launch, does login work, does the homepage load, run right after a new build before investing time in deeper testing. Sanity testing is narrower and deeper, run after a specific bug fix or small change, to confirm that particular area of the application now behaves correctly without necessarily re-checking the whole application.

The distinction that trips people up: smoke is broad and shallow, covering many areas briefly. Sanity is narrow and focused, covering one area in more depth after a targeted change.

API tests run faster, since there's no browser to launch and no rendering to wait on, and they're far less brittle, since they don't break every time a frontend team reshuffles a CSS class or adds a wrapping div. They also isolate failures better: an API test failure points directly at a broken endpoint or business rule, while a UI test failure could mean the backend broke, the frontend broke, or the test itself is timing out for an unrelated reason.

This maps directly onto the test pyramid's reasoning: push validation as low as it can reasonably go, and reserve UI tests for the handful of critical end-to-end flows that genuinely need to be checked through the browser.

@BeforeEach runs before every single test method, ideal for resetting state that must be clean per test. @BeforeAll runs once before any test in the class runs, meant for expensive setup that's safe to share across tests, spinning up a database connection, say.

java
@BeforeAll
static void setupDb() { connection = createTestDbConnection(); }

@BeforeEach
void resetData() { connection.execute("TRUNCATE orders"); }

A real bug from picking wrong: putting data-clearing logic in @BeforeAll instead of @BeforeEach means the first test in the class runs against clean data, but every subsequent test in that class inherits whatever state the previous test left behind, silent cross-test contamination that only shows up as flaky, order-dependent failures that are miserable to track down.

Functional testing checks whether the application does what it's supposed to do, does clicking submit actually place the order, does the search return the right results. Non-functional testing checks how well it does it, how fast, how it holds up under load, how secure it is, how usable it is for someone relying on a screen reader. A feature can pass every functional test and still be unusable in production if it's too slow or fails under real traffic, which is exactly why both categories get their own dedicated test coverage rather than one covering for the other.

Load testing checks how the system behaves under an expected, realistic traffic level, does it stay within acceptable response times at the volume you actually expect in production. Stress testing pushes traffic well beyond expected levels specifically to find the breaking point, where does the system actually fail, and does it fail gracefully, queueing or rejecting requests cleanly, or does it fall over in a way that takes down unrelated functionality with it.

Push every opening bracket onto a stack. On a closing bracket, pop and check it matches the corresponding opening bracket, if the stack is empty or the popped value doesn't match, the string is unbalanced. At the end, the stack must be empty for the string to be balanced.

python
def is_balanced(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for char in s:
        if char in '([{':
            stack.append(char)
        elif char in ')]}':
            if not stack or stack.pop() != pairs[char]:
                return False
    return not stack

Interviewers use this to check whether a candidate reaches for a stack naturally, and whether they remember the two edge cases everyone forgets on the first pass: an empty string is balanced, and leftover unmatched opens at the end make it unbalanced even if every close matched something.

Convert to a string, reverse it, convert back, or do it purely with arithmetic using modulo and integer division. The edge case that trips people up: negative numbers, and integer overflow in languages with fixed-width integers, where reversing a large number can produce a result outside the valid range for that type.

python
def reverse_int(n):
    sign = -1 if n < 0 else 1
    n = abs(n)
    reversed_n = int(str(n)[::-1])
    return sign * reversed_n

In Python this overflow edge case doesn't actually matter, integers aren't fixed-width. The interviewer is often testing whether you know that difference, not whether your Python code happens to work.

The recursive definition is straightforward, but the edge cases interviewers actually check are: factorial of 0 should return 1 by definition, and negative input has no valid factorial, so the function should raise an error rather than silently recursing forever or returning a nonsensical value.

python
def factorial(n):
    if n < 0:
        raise ValueError("factorial undefined for negative numbers")
    if n == 0:
        return 1
    return n * factorial(n - 1)

For very large n, a candidate who mentions recursion depth limits, and that an iterative version avoids stack overflow for large inputs, is showing they think about the failure mode, not just the happy path.

Medium questions

25

Retesting confirms that a specific reported bug is actually fixed, you run the exact scenario that failed before and check it now passes. Regression testing checks that the fix, or any other recent change, didn't break something unrelated elsewhere in the system. They're both run after a code change, but retesting is scoped to one known defect while regression testing is scoped to everything that might have been affected by the change, including features nobody touched directly.

Exploratory testing is simultaneous learning, test design, and execution, a tester interacts with the application without a scripted test case, following their judgment about where bugs are likely to hide. Automation is good at repeating a known scenario reliably forever. It's bad at noticing something weird that nobody thought to write a test for in the first place.

Even on a team with strong automation coverage, exploratory sessions before a major release still catch real issues, especially around new UI flows where the "obvious" test cases haven't been written yet because the feature is new. The two approaches aren't competing, exploratory testing feeds the backlog of what to automate next.

An implicit wait is set once, globally, on the driver, and applies to every subsequent element lookup: the driver polls for up to that duration before throwing a not-found error. An explicit wait is scoped to a single condition, you tell it exactly what to wait for, an element being clickable, visible, or containing certain text, and it polls until that condition is true or the timeout expires.

java
// implicit wait, applies to every findElement call for the session
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));

// explicit wait, scoped to one specific condition
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit")));

Selenium's own docs are direct about mixing the two: don't. Combining implicit and explicit waits in the same test can produce unpredictable timeout behavior, because the driver ends up waiting on both mechanisms at once in ways that aren't well defined (Selenium documentation). A framework with a global implicit wait and scattered explicit waits sprinkled in is a common source of the exact flaky failures interviewers ask about.

ID is the most stable, IDs are usually unique and don't shift with layout changes. CSS selectors and name attributes come next, reasonably stable unless the frontend team restructures markup. XPath, especially absolute XPath written with the full path from the document root, is the most brittle, since it breaks the moment anyone adds a wrapping div anywhere above the target element.

java
// brittle: absolute path, breaks if one ancestor div shifts
driver.findElement(By.xpath("/html/body/div[2]/div[1]/form/button[3]"));

// far more stable: unique id, or a relative xpath anchored on a stable attribute
driver.findElement(By.id("checkout-submit"));
driver.findElement(By.xpath("//button[@data-testid='checkout-submit']"));

Adding dedicated test attributes like data-testid that the frontend team agrees not to change casually is the most reliable long-term fix, and it's a conversation worth having with frontend engineers early rather than fighting brittle selectors for months.

Page Object Model wraps each page or component's locators and interactions behind a class, so test code calls methods like loginPage.login(user, pass) instead of scattering raw findElement calls and locator strings across every test file. The problem it solves is maintenance: when the UI changes, a locator update happens in exactly one place, the page object, instead of in every test that happens to touch that element.

java
public class LoginPage {
  private WebDriver driver;
  private By username = By.id("username");
  private By password = By.id("password");
  private By submit = By.id("login-submit");

  public LoginPage(WebDriver driver) { this.driver = driver; }

  public void login(String user, String pass) {
    driver.findElement(username).sendKeys(user);
    driver.findElement(password).sendKeys(pass);
    driver.findElement(submit).click();
  }
}

Without it, a single CSS class rename can require touching dozens of test files. With it, that same change is a one-line fix in the page object.

Headless execution runs the browser engine without rendering an actual visible window, which is faster and lighter on CI infrastructure since there's no display server to manage. Headed execution renders the real browser UI, useful for local debugging where you want to watch a test run and see exactly where it breaks.

Some rendering or CSS-dependent bugs only show up in headed mode, since certain layout engines behave slightly differently without a real display context, so a suite that only ever runs headless can miss visual regressions. Most teams run headless in CI for speed and keep a headed mode available locally for debugging, rather than picking one mode exclusively.

401 Unauthorized means the request lacks valid authentication entirely, no token, an expired token, invalid credentials. 403 Forbidden means the caller is authenticated just fine, but isn't allowed to perform this specific action or access this specific resource.

text
GET /admin/users with no auth header       -> expect 401
GET /admin/users with a valid, non-admin token -> expect 403

A test that only checks "the request was rejected" without asserting the specific status code can pass even when an authorization bug flips these two, a permissions check that returns 401 instead of 403 might be leaking information about whether a resource exists at all to an unauthenticated caller, which is a real security gap that a loose assertion would never catch.

REST uses standard HTTP verbs and status codes, and responses are typically JSON, so testing leans heavily on asserting status codes, response schema, and headers. SOAP wraps every request and response in an XML envelope with a stricter, more formal contract defined by a WSDL file, so testing SOAP often means validating against that schema directly and paying more attention to the envelope structure, not just the payload.

In practice REST dominates new API test suites in 2026, but plenty of SDET roles at larger, older enterprises still maintain SOAP test coverage for legacy services, and the assumption that "API testing" always means REST is worth checking with the interviewer if the job description doesn't say.

Mocking replaces the real dependency with a stand-in that returns controlled, predictable responses, fast, deterministic, and not affected by the third party's own uptime or rate limits. Hitting a real staging environment tests actual integration behavior, including edge cases in the real service's response format that a hand-written mock might not anticipate.

The tradeoff: mocks give you speed and determinism but can drift from what the real service actually does if nobody updates them when the real API changes. Staging environments are more realistic but slower, flakier, and often shared across teams in ways that cause noisy, unrelated failures. Most mature suites use both: mocks for fast unit and component-level tests, a smaller set of tests against a real staging or sandbox environment to catch drift.

Data-driven testing runs the same test logic repeatedly against different sets of input data, usually pulled from a CSV, spreadsheet, or parameterized test annotation, so one test method covers many scenarios by varying the data. Keyword-driven testing abstracts the test steps themselves into reusable keywords, like "login," "add to cart," "checkout," that non-programmers can assemble into a test case without touching the underlying code.

java
@ParameterizedTest
@CsvSource({"admin,true", "guest,false", "banned,false"})
void loginAccessTest(String role, boolean expectedAccess) {
  assertEquals(expectedAccess, canAccessDashboard(role));
}

Data-driven is the more common pattern on engineering-heavy SDET teams. Keyword-driven shows up more on teams where non-technical testers need to build test cases themselves, since it trades some flexibility for approachability.

BDD frameworks let you write test scenarios in a structured natural-language format, Given/When/Then, that's meant to be readable by product owners and business stakeholders, not just engineers, so the same document doubles as a spec and an executable test.

gherkin
Given a user with an empty cart
When they add a product priced at $20
Then the cart total should be $20

The failure mode teams hit constantly: the Gherkin file drifts from something a business stakeholder actually reads into something only engineers ever touch, at which point you're maintaining an extra translation layer, the natural-language step, plus the step-definition code behind it, with no one actually reading the natural-language part anymore. When that happens the collaboration benefit BDD was supposed to give you is gone, and you're just paying the overhead of an extra abstraction layer on top of a normal test.

A fixture is the known, controlled state a test needs before it runs, seeded database rows, a mock server response, a specific user account. Bad fixture management, tests that share the same database rows, tests that mutate shared fixture data without cleaning up after themselves, is one of the biggest sources of order-dependent flakiness in a suite: test A passes alone, fails when run after test B, because B left the shared fixture in a state A didn't expect.

The fix is usually isolating fixtures per test, or per test class at minimum, and tearing state back down after each run rather than assuming the next test will get a clean slate on its own.

Fast, cheap tests, unit tests and linting, run on every single commit or pull request, since they finish in seconds and catch the majority of regressions cheaply. Slower integration and API tests typically run on merge to a shared branch or on a scheduled interval. The slowest, most expensive tests, full end-to-end UI suites and performance tests, often run on a nightly schedule or right before a release, not on every commit.

Running the full suite on every commit sounds thorough but in practice slows every engineer down waiting on CI, which pushes people toward skipping tests locally and just waiting for CI to tell them what broke, the opposite of the fast-feedback loop testing is supposed to give you.

The decision usually comes down to how confident the team is in the test's signal. A test covering a core business flow, checkout, login, payment, that has a strong track record of only failing when something is genuinely broken should block the deploy without exception. A test that's known to be flaky, or that covers a low-risk edge case, generating a warning that a human reviews rather than an automatic hard block keeps the pipeline moving without silently ignoring real signal.

The trap teams fall into is letting too many tests sit in "warning, not blocking" limbo. Once that list gets long enough, a genuine regression on a warning-only test gets scrolled past the same way everything else on that list does, and the whole point of having the test evaporates.

A deploy-blocking smoke suite should cover only the paths that would be catastrophic if broken and that real users hit constantly, login, checkout, the core page load, nothing more. The moment it grows past ten or fifteen minutes, it's stopped being a smoke suite and has become a slow gate that people start working around.

Keeping it small takes active maintenance, not a one-time decision. Every time someone proposes adding "just one more" test to the blocking suite because a recent incident happened there, I'd push back and ask whether it belongs in the faster nightly regression run instead. Smoke suites bloat exactly one test at a time, each addition individually reasonable, until the whole thing is thirty minutes and nobody remembers agreeing to that.

Test data management is the strategy for creating, seeding, and cleaning up the data a test suite depends on, without one test's data corrupting another's. Locally, one developer usually has exclusive access to their own database instance, so stale or leftover data from a previous run is easy to spot and reset by hand.

In CI, multiple pipeline runs can execute concurrently against a shared test database, or the same runner gets reused across unrelated jobs without a full reset in between, so leftover data from a failed run can silently corrupt the next run's assumptions. Solutions that hold up: spinning up an isolated, ephemeral database per pipeline run, or namespacing all test data per run with a unique identifier so parallel runs can't collide even on a shared instance.

Looking only at the average response time instead of the tail, the 95th or 99th percentile. An average can look perfectly healthy while a meaningful slice of real users are hitting response times several times slower than that average, because a handful of very slow outliers get smoothed out by a much larger number of fast requests when you just average them together.

A system with a 200ms average and a 4-second p99 has a real problem that the average completely hides. Reporting percentiles, not just the mean, is the detail that separates a performance test result someone can actually act on from one that just looks reassuring on a dashboard.

Accessibility testing checks whether the application works for users relying on assistive technology, screen readers, keyboard-only navigation, sufficient color contrast. A full accessibility audit is a specialized discipline, but a reasonable baseline can ride along in an existing Selenium or Playwright suite: automated axe-core scans run against each page under test catch a meaningful share of common violations, missing alt text, insufficient contrast, missing form labels, without requiring a dedicated accessibility test suite from scratch.

With numbers, addition and subtraction, or XOR, can swap two values without a third variable. With XOR specifically: a becomes a XOR b, then b becomes a XOR b (which is now the original a, XORed with itself against original b, isolating original a), then a becomes a XOR b again to recover original b.

python
a, b = 5, 10
a = a ^ b
b = a ^ b
a = a ^ b
# a is now 10, b is now 5

In real production code I'd never actually do this. A temporary variable, or in Python just a, b = b, a, is instantly readable to the next person touching the code. The XOR trick is a fun interview exercise that exists because it used to matter on memory-constrained embedded systems decades ago, not because it's good practice on a modern team.

Sort the array first, then for each element, use a two-pointer sweep across the rest of the array to find pairs that sum to the target minus that element. Sorting brings this down from a brute-force triple nested loop to a much better bound, and the two-pointer technique only works cleanly once the array is sorted.

python
def three_sum(nums, target):
    nums.sort()
    result = []
    for i in range(len(nums) - 2):
        left, right = i + 1, len(nums) - 1
        while left < right:
            total = nums[i] + nums[left] + nums[right]
            if total == target:
                result.append([nums[i], nums[left], nums[right]])
                left += 1
                right -= 1
            elif total < target:
                left += 1
            else:
                right -= 1
    return result

Watch for duplicate triplets in the output if the array has repeated values, a follow-up question interviewers ask almost every time once the base solution works.

The nested-loop approach, for each character, scan the rest of the string counting occurrences, runs in quadratic time. A single pass building a character-count map, followed by a second pass checking each character's count in that map, gets it down to linear time at the cost of extra space for the map.

python
def first_unique_char(s):
    counts = {}
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1
    for ch in s:
        if counts[ch] == 1:
            return ch
    return None

Test both states of the flag explicitly rather than relying on the default state alone, since a flag flipped on in production for one segment and off for another means the code path most engineers exercise locally isn't necessarily the one real users are hitting. I'd parameterize the test suite to run the relevant scenarios once with the flag on and once with it off, rather than writing entirely separate test files.

I'd also specifically test the flag's rollout boundary itself, users right at the edge of a percentage rollout, and the transition behavior when a flag gets flipped mid-session for a user already using the app, since that's the scenario most teams forget to think about until a real user reports something broken that only happens if the flag changes state while they're actively using the feature.

Coverage percentage measures which lines executed during tests, not whether the behavior at those lines was actually verified correctly, so chasing a specific number as the goal produces tests that touch a line without meaningfully asserting anything about it. I look instead at risk: which code paths handle money, security, or data integrity, and which paths get exercised by real user traffic constantly versus a rarely-used edge case.

A reasonable target in practice is high coverage on business-critical logic and lower, more selective coverage on straightforward glue code or generated boilerplate, rather than a single blanket number applied uniformly across a whole codebase. A team that mandates 90% coverage everywhere often gets exactly 90%, achieved by testing getters and setters that never had a bug in the first place, while the genuinely risky logic sits under-tested because it was harder to test and nobody was measuring whether the coverage that existed was actually meaningful.

Beyond the obvious happy path, checking that page 1 returns the expected page size and a valid next-page token, I'd specifically test the edges: requesting a page number beyond the total result count, an explicit page size of zero or a negative number, and whether the total count in the response actually matches the real number of underlying records rather than an off-by-one from the pagination logic.

I'd also check consistency across pages when the underlying data changes between requests, does inserting a new record mid-pagination shift results and cause a duplicate or a skipped record on the next page. That's the bug pagination logic gets wrong most often in practice, and it rarely shows up in a quick manual click-through.

I'd start with layering, not tool choice: a thin driver-abstraction layer so the tests themselves don't call Selenium or an HTTP client directly, a page-object or API-client layer above that, and the actual test files calling into those, not into raw framework calls. That separation is what lets you swap tools later, moving from Selenium to Playwright, say, without rewriting every test.

I'd push hard for the bulk of coverage to sit at the unit and API layer from day one rather than defaulting to UI tests because they're the most obviously visible form of "testing" to non-engineers. I'd also build in reporting and CI integration early, a suite nobody looks at because the results live in a log file nobody opens gets ignored within a month, no matter how good the tests themselves are. Retrofitting reporting onto an existing framework six months in is a much bigger project than building it in from the start.

Hard questions

10

Selenium's WebElement object is a reference to a specific node in the DOM at the moment it was located, not a live pointer that automatically tracks the page. If the DOM gets rebuilt, a re-render, an Angular or React state update, an AJAX refresh that swaps out a container, the old node is destroyed and a visually identical new one takes its place. The WebElement you're holding still points at the destroyed node, so any interaction with it throws StaleElementReferenceException, even though something that looks exactly the same is sitting right there on screen.

The fix is re-locating the element right before you interact with it rather than caching a WebElement reference across steps that might trigger a re-render. It's one of the clearest examples in Selenium of why treating a page like a static document instead of a live application causes tests to break in ways that confuse people new to browser automation.

A flaky test passes and fails on the exact same code with no relevant change in between, which usually points to the test itself, a race condition, shared test data, order dependency, rather than the product. A test correctly catching a real bug fails consistently under the same conditions that trigger the actual defect, and a fix to the product, not the test, makes it pass reliably again.

The way to tell them apart is to run the failing test in isolation, repeatedly, under the same conditions. If it fails 100% of the time when run alone but only sometimes in the full suite, that's usually shared state or ordering, not a real product bug. If it fails inconsistently even fully isolated, look for a genuine race condition in the product itself, an async operation the UI doesn't properly wait on before rendering. Quarantining a test to "known flaky, ignore" without doing this investigation first is how real bugs quietly stop getting caught.

Contract testing verifies that a consumer service and a provider service agree on the shape of the API between them, request format, response schema, required fields, without spinning up both services together in a full integration environment. A consumer-driven contract test captures what the consumer actually expects from the provider, then that same contract gets replayed against the real provider in its own pipeline to confirm it still honors it.

The reason to add it on top of integration tests: full integration tests across service boundaries are slow, require standing up dependent services, and often live in a separate repo from the team that owns the change that just broke them, so a provider team can ship a breaking change and only find out when a downstream team's tests fail hours or days later. Contract tests catch the same class of breakage fast, inside each team's own pipeline, without needing the other service running at all.

An idempotent endpoint produces the same result no matter how many times the same request is sent, calling it once or five times leaves the system in the same state. GET, PUT, and DELETE are supposed to be idempotent by the HTTP spec; POST typically isn't, since a repeated POST usually creates a new resource each time.

text
PUT /orders/123 { status: "shipped" }
// call it once, call it three times in a row
// state after 1 call should equal state after 3 calls

A real test sends the identical request multiple times and asserts the final state and side effects are the same regardless of call count, not just that each individual response returned 200. This matters most for payment and order-status endpoints, where a client retry after a network timeout must not double-charge or double-ship because the endpoint wasn't actually idempotent under the hood.

Multi-machine parallelization splits the whole test suite across separate CI runners, each with its own isolated environment, so tests can't interfere with each other through shared local state. Multi-threaded parallelization on one machine runs multiple tests concurrently within the same process or environment, faster to set up but far more exposed to shared state, a shared database connection, a shared temp file, a static variable that isn't thread-safe.

The first thing that breaks in a suite not designed for parallel execution is shared test data: two tests that both create a user with the same hardcoded email address will race and one will fail with a duplicate-key error that has nothing to do with the actual feature being tested. The fix is generating unique test data per test run, a random suffix on emails and IDs, rather than hardcoding fixture values that assume exclusive access.

Run a soak test, sustained, moderate load over an extended period, hours rather than minutes, while sampling the process's memory usage at regular intervals. A service without a leak should show memory usage rise initially and then plateau as garbage collection or normal cleanup keeps pace. A service with a leak shows memory climbing steadily with no plateau, even under constant, unchanging load.

Without production dashboards, tools like a language runtime's built-in heap profiler, or simply capturing process memory stats on a timer during the soak test and graphing them afterward, are usually enough to spot the pattern. The key detail interviewers want to hear is "sustained load over time," not a short burst, since a short load test frequently doesn't run long enough for a slow leak to become visible against normal memory fluctuation.

A hash set gives linear time at the cost of linear extra space, insert each element and check membership before inserting, the first element already present is a duplicate. To use less memory, if the array's values are constrained to a known range (say, integers from 1 to n for an array of length n), you can use the array itself as a marker, negating the value at the index corresponding to each number seen, then any index whose value is already negative when you go to negate it again reveals a duplicate.

python
def find_duplicate(nums):
    for num in nums:
        idx = abs(num) - 1
        if nums[idx] < 0:
            return abs(num)
        nums[idx] = -nums[idx]
    return -1

The tradeoff is real: the in-place approach mutates the input array, which is unacceptable if the caller needs the original data intact afterward, and it only works under that specific value-range constraint. A hash set works on any input with no constraint, at the cost of extra space. I'd default to the hash set unless memory was a demonstrated, measured constraint.

More runners helps but doesn't fix the underlying problem if the suite is testing the wrong things at the wrong layer. I'd start by auditing what each end-to-end test is actually verifying, and for any test whose assertion could be made just as confidently at the API or unit layer, without needing a real browser, I'd push it down the pyramid instead of just paying to run it faster. A lot of "end-to-end" suites accumulate tests that never needed a browser in the first place, they were just easiest to write that way at the time.

After that pruning, I'd parallelize what's left across runners, and separate the suite into a small, fast smoke set that blocks every deploy versus a larger nightly regression set that runs on a schedule and doesn't block anyone in the critical path. The goal is a deploy-blocking suite measured in minutes, not tens of minutes, with the comprehensive coverage still running, just not gating every single release.

A stub returns a fixed, predetermined response when called, it doesn't verify anything about how it was called. A mock does verify interactions, it can assert that it was called a specific number of times, with specific arguments, and fail the test if that expectation isn't met. A fake is a working, simplified implementation of the real thing, an in-memory database standing in for a real one, that behaves correctly but skips the production-grade complexity (Martin Fowler, Practical Test Pyramid).

The confusion matters in practice because these three have different failure modes. Overusing mocks that assert on exact call arguments makes tests brittle to harmless refactors, since a test can fail even when behavior is unchanged just because an internal call pattern shifted. A team that calls everything "a mock" regardless of which of the three it actually is tends to reach for the most rigid option by default, and ends up with a test suite that breaks constantly on refactors that didn't change any actual behavior.

First I'd check whether it's the same test failing every time or a different one each run, that alone tells you whether you're looking at a specific timing bug or environmental flakiness across the board. Then I'd look at what's different about CI versus local: usually slower or shared hardware, headless mode instead of a real browser window, different screen resolution affecting element visibility, or parallel test runs competing for the same test data.

The most common root cause in practice is a race condition between the test and an async UI update, a modal that takes longer to render under CI load than it does on a fast local machine, so a click fires before the element is actually interactable. I'd add explicit waits tied to the actual condition (element clickable, network request finished) rather than bumping a global timeout number, since a longer timeout just delays the failure without fixing the race. Screenshots and browser console logs captured on failure, uploaded as CI artifacts, turn a "it's flaky" report into an actual root cause most of the time.

How to prepare for an SDET interview

Skip re-reading a list of test-pyramid definitions the night before. Build one small thing that forces you to make real tradeoffs: a Page Object Model wrapper around a login flow with an explicit wait instead of a sleep, an API test suite for a public endpoint that checks status codes and idempotency, a parallel-safe test data setup that generates unique fixtures per run instead of hardcoding values. Watching your own suite go from flaky to reliable teaches more about waits, fixtures, and race conditions than reading about them ever will.

Across mock interviews run through LastRoundAI, the flaky-test debugging question trips up more candidates than the coding round does, mostly because candidates prep coding problems on purpose but rarely rehearse explaining a debugging process out loud under time pressure. Practicing the explanation, not just knowing the answer in your head, is the part most people skip.

One more thing worth knowing: SDET roles increasingly expect comfort across the whole stack, unit tests in the application's own language, API tests in a separate service-layer suite, and enough CI/CD literacy to own pipeline integration rather than handing that off to a separate DevOps team. A candidate who can only speak to UI automation is competing for a narrower slice of the roles actually open in 2026 than one who can speak to all three layers.

Get the reps in before the real thing

Explaining why a test is flaky on paper is not the same as defending your debugging process out loud when an interviewer keeps pushing "but what if it's not that." LastRoundAI's mock interview mode runs live technical rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19/mo if a handful of sessions isn't enough runway.

Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough SDET and QA automation roles that actually test the skills this page covers instead of just asking for years of Selenium on a resume. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

Leave a Reply

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