Automation developer interview questions
Questions an interviewer can ask a Automation developer, grouped by years of experience. Open one to see the strongest answer and why it works.
48 of the 300 questions our bank holds for this job. The rest are in the app.
By subject
- Programming
- Automation Fundamentals
- Web Automation
- API Automation
- Mobile Automation
- Framework Design
- CI/CD
- Performance Testing
- Database Testing
- Quality Engineering
0–1 years
A test fails on a slow CI runner. A teammate adds sleep(5000). What do you do instead?
The strongest answer: Wait for the condition that means the page is ready — a visible name, a network idle you can name, a disabled button becoming enabled — not a guessed number of seconds
Why it works: Exactly. Wait for a signal. Sleep is a guess that is too short on a bad day and too slow on a good one.
- Wait for a named ready condition.
- Sleep is a guess, not a wait.
- Same wait policy on laptop and CI.
- Do not delete the oracle to go green.
def add(item, bag=[]): bag.append(item); return bag — you call add(1) then add(2). What comes back the second time?
The strongest answer: [1, 2] — the default list is created once when the function is defined, so every call without a bag shares it; use bag=None and build a new list inside
Why it works: Exactly. One default object, shared for the life of the function. None plus a fresh list inside is the fix.
- Default arguments are evaluated once, at definition.
- A mutable default is shared by every call.
- Use None and create the value inside the function.
- The bug is silent, not an error.
Where should most automated checks live?
The strongest answer: In fast, isolated tests — units and API contracts — with a thin UI smoke on the money path, not a mountain of browser clicks for every field
Why it works: Exactly. Cheap layers catch cheap bugs. The UI pack is a scarce budget.
- Many cheap tests, few browser tests.
- UI pack = money path.
- Do not skip the glue.
- Tools follow the pyramid, not the reverse.
What is a regression in this interview?
The strongest answer: Something that used to work and now does not — I re-check the money paths after a change, I do not only poke the new button
Why it works: Exactly. Old behaviour broken by a change. That is why we keep a regression pack.
- Regression = used to work.
- Re-check money paths after a change.
- Keep a small, honest pack.
- Automation is for the boring repeats of that pack.
When would you choose a tuple over a list?
The strongest answer: When the collection is fixed and its position means something — a coordinate, a record, a dict key — since tuples are immutable and therefore hashable
Why it works: Right. Immutability is the point, and hashability is what it buys you.
- Tuple = fixed, positional, hashable.
- List = a sequence you intend to change.
- Only a tuple can key a dict or join a set.
- Choose on meaning, not on length.
Which locator do you reach for first?
The strongest answer: A role, label, test id, or other name the product owns — not nth-child, a pixel offset, or a CSS class that is a styling leftover
Why it works: Exactly. Prefer what a user or a contract would name. Styling leftovers move.
- Role, label, or test id first.
- Do not use nth-child as a habit.
- Recorded locators get edited.
- Visible copy is a weak contract.
A bug “works on my machine” and fails in staging. What do you write first?
The strongest answer: The environment, build, and the data I used — “staging, build 184, user with an empty cart” — not another screenshot of the same screen
Why it works: Exactly. Env + build + data is how “works on my machine” becomes a bug.
- Write env, build, data.
- Do not assume “the” environment.
- Staging ≠ local ≠ prod.
- Automation reports the same three.
A page shows “Something went wrong.” What does the Network tab tell you before you file the bug?
The strongest answer: Which request failed, its status code, and whether a response arrived at all — so the bug says “POST /orders returned 500”, not “it broke”
Why it works: Exactly — the failing call, its status and its response turn a vague report into one a developer can start on.
- Open the Network tab before filing.
- Find the failing request.
- Read method, path and status.
- Put them in the bug title.
A new build lands. What do you run first?
The strongest answer: A short smoke of install, login, and the money path — I do not start a 4-hour suite on a build that cannot open
Why it works: Exactly. Smoke is a cheap “is this a build.” Then go deep.
- Smoke first: install, login, money.
- Then the planned depth.
- Automate the smoke if you can.
- Do not start long if launch is dead.
What is the difference between == and is?
The strongest answer: == asks whether two values are equal; is asks whether they are the very same object — use is only for None, True, False and sentinels
Why it works: Exactly. Identity versus equality, and is belongs to singletons.
- == compares values, is compares identity.
- Use is None, not == None.
- Equal objects need not be the same object.
- Caching makes is look right by accident.
A field accepts 1–100. Which values do you try first?
The strongest answer: 0, 1, 100, 101, and an empty / non-numeric — the edges and just outside, not 47 because it “feels typical”
Why it works: Exactly. Boundaries and just-outside catch the off-by-one. The middle is a later sample.
- Min, max, just inside, just outside.
- Empty and the wrong type.
- Do not start in the middle.
- Automation should encode those edges too.
You need to prove a discount rule. Where do you put the first automated check?
The strongest answer: On the API or the pricing function — same inputs, same outputs — and keep one UI case for the badge the shopper sees
Why it works: Exactly. The rule is data in, price out. The UI is one witness, not the only one.
- Test the rule where it lives.
- One UI witness for the shopper.
- Do not wait on a visual licence.
- Spreadsheets are not CI.
1–3 years
When do you reach for screenplay / actor + task instead of a page object?
The strongest answer: When the same actor does the same job across many screens — “checkout as a returning buyer” — and page objects are turning into dump trucks of methods
Why it works: Exactly. Screenplay is for repeated actor-jobs. Page objects still win for a single screen’s API.
- Actors + tasks for cross-screen jobs.
- Page objects for one screen’s API.
- Do not fashion-ban POM.
- Do not use a pattern to hide asserts.
You read a 40 GB log file to count matching lines. What shape does the code take?
The strongest answer: Iterate the file object line by line, or write a generator that yields matches — one line is in memory at a time, so the file size stops mattering
Why it works: Exactly. Streaming turns memory cost into a constant.
- Iterating a file yields lines lazily.
- Generators keep memory constant.
- readlines and read materialise everything.
- yield lets you compose stages without buffering.
How do you debug a flake that only happens in CI?
The strongest answer: Reproduce with the trace, clock, and seed: wait, shared data, time, and parallelism — I do not add sleep and close the ticket
Why it works: Exactly. Flakes have causes: time, data, waits, isolation. Traces exist so you stop guessing.
- Use the trace and the seed.
- Check waits, data, time, parallel.
- Do not timeout your way out.
- Do not outsource the cause.
You have automated the happy path for POST /orders. Which negative cases go in next, and why those?
The strongest answer: No token and a wrong-role token — 401 against 403 — a missing required field, a malformed body, and an unknown product id; each has its own status and an error code clients branch on
Why it works: Right — and separating 401 from 403 is the check teams most often skip.
- One negative case per rule.
- Separate 401 from 403.
- Assert the error code, not the prose.
- Skip the combinatorial explosion.
What is a decorator, really?
The strongest answer: A function that takes a function and returns a replacement — @log above def f is just f = log(f), which is why the wrapper should carry functools.wraps
Why it works: Right, and wraps is the part people leave out until a name or docstring goes missing.
- @d over f means f = d(f).
- The decorator runs at definition time.
- functools.wraps keeps __name__ and __doc__.
- A decorator with arguments is one more layer.
You turn on 8 workers and 20 tests collide on the same user. What was missing?
The strongest answer: Per-test identity — users, carts, and rows namespaced by worker or uuid — parallelism without isolation is a race you scheduled
Why it works: Exactly. Parallel is a data-design problem. Workers are cheap; unique fixtures are the work.
- Namespace data per worker / test.
- Parallel without isolation is a race.
- Do not mutex the whole suite.
- Docs are not isolation.
Walk me through what happens between your Appium test calling click and the button being tapped on the device.
The strongest answer: The client library sends a WebDriver command over HTTP to the Appium server, which routes it to the platform driver for that session — UiAutomator2 on Android, XCUITest on iOS — and that drives the app and returns the result
Why it works: Right — client, server, platform driver, device. Knowing the hops is how you read an Appium stack trace.
- Client, server, platform driver, device.
- Commands travel as WebDriver over HTTP.
- UiAutomator2 on Android, XCUITest on iOS.
- Read failures by naming the hop.
You need setup and guaranteed teardown around a block. What do you write?
The strongest answer: A context manager — @contextlib.contextmanager with a yield between setup and cleanup in a finally, or a class with __enter__ and __exit__
Why it works: Exactly, and the finally around the yield is what makes it hold on the error path.
- with guarantees cleanup on every exit.
- @contextmanager: setup, yield, cleanup in finally.
- Or __enter__ / __exit__ on a class.
- __exit__ can suppress an exception — usually it should not.
A country dropdown has 180 values. How do you test it without 180 cases?
The strongest answer: Equivalence: one typical, the empty, a long name, a right-to-left locale, and any country that changes a rule (tax, shipping) — plus the boundary of “none selected”
Why it works: Exactly. Classes, then the ones that change a rule. That is the craft.
- Group equivalents.
- Add the rule-changers.
- Add empty and long / RTL.
- Automate the rule-changers if you can.
What is a consumer-driven contract test for?
The strongest answer: To prove the provider still speaks the shape the consumer coded against — without standing up the whole UI or the whole fleet
Why it works: Exactly. It is a cheap conversation between two services. It is not the whole integration story.
- Prove the shape the consumer needs.
- Keep a few real integrations.
- Not traffic generation.
- Not an uptime check.
A teammate wants the suite’s test data in a spreadsheet so “anyone can add a case”, starting with 400 rows for the checkout journey.
What do you push back on?
The strongest answer: Data-driven is one journey with genuinely different inputs — one row per rule, each named so a failure says which rule broke. Four hundred rows of the same path is runtime, not signal, and the file belongs in the repo
Why it works: Right — a row has to be a rule, it has to be named, and it has to be versioned with the code.
- One row per rule, not per repetition.
- Name the row so failures read.
- Keep the data in the repo.
- More rows is not more coverage.
Can you rely on a dict keeping insertion order?
The strongest answer: Yes since Python 3.7 it is part of the language spec — but if the order is meaningful to the reader, say so, and use OrderedDict when you need order-sensitive equality or move_to_end
Why it works: Right, and the caveat about equality is the part that still matters.
- Insertion order is guaranteed from 3.7.
- Sets are still unordered.
- OrderedDict for order-sensitive equality and move_to_end.
- Say it in the code when order carries meaning.
3–5 years
The UI pack is 70% of runtime and finds 10% of bugs. What do you do?
The strongest answer: Move cases down — API, contract, unit — and keep UI for journeys a lower layer cannot see; I do not ask for twice the runners first
Why it works: Exactly. Runtime vs bugs is the rebalance trigger. Hardware is the last lever.
- Move cases down the pyramid.
- Keep UI for true journeys.
- Runners last.
- Do not fashion-ban layers.
A CPU-bound function is too slow. You add threads and nothing improves. Why?
The strongest answer: The GIL lets only one thread execute Python bytecode at a time, so threads help blocking I/O, not computation — use processes, a native library that releases the GIL, or move the work out of Python
Why it works: Exactly. Threads for waiting, processes for computing.
- The GIL serialises bytecode execution.
- Threads still win on I/O waits.
- multiprocessing for CPU-bound work.
- NumPy and friends release the GIL in C.
How do you talk about flakes with a director?
The strongest answer: As a budget — a visible rate, a quarantine SLA, and the merge-delay they buy — not “CI is moody”
Why it works: Exactly. Flakes are a tax you can name in time and trust. Mood is not a metric.
- Name the rate and the delay.
- Give a quarantine SLA.
- Do not joke it away.
- Own the causes you control.
Your k6 script runs nightly and produces a dashboard nobody has opened in a month.
How do you turn it into something that can fail?
The strongest answer: Put thresholds in the script — a ceiling on the p95 duration and on the failed-request rate — so k6 exits non-zero when they are breached and the pipeline goes red like any other test
Why it works: Exactly — a performance run with no pass condition is a picture; thresholds make it a check.
- A run with no pass condition is a picture.
- Threshold the tail and the error rate.
- A non-zero exit turns the build red.
- Take the target from the requirement.
One synchronous database call inside an async handler makes the whole service slow. Why?
The strongest answer: A blocking call stops the event loop, so no other coroutine can run until it returns — use an async driver, or push it to a thread with asyncio.to_thread
Why it works: Right. One blocking call in an async service is an outage waiting for load.
- Blocking code freezes the whole event loop.
- Use async drivers end to end.
- asyncio.to_thread for unavoidable blocking calls.
- One offender starves every other coroutine.
When do you validate a response against a JSON schema, and when do you assert on individual fields?
The strongest answer: Schema for the shape — types, required fields, enums — so a renamed or dropped field fails everywhere; field assertions for the values this particular test is about. One schema per response, a few values per test
Why it works: Exactly — shape and value are different questions, and one schema replaces dozens of type checks.
- Schema checks shape; asserts check values.
- One schema per response body.
- Assert only the values under test.
- A dropped field must fail something.
Three independent API calls run one after another in an async function. How do you overlap them?
The strongest answer: await asyncio.gather(a(), b(), c()) so all three are in flight at once — and decide what return_exceptions should do, because by default the first failure propagates
Why it works: Exactly, and the exception policy is the part usually left unconsidered.
- gather runs awaitables concurrently.
- Sequential awaits are sequential.
- return_exceptions decides fail-fast or collect.
- TaskGroup gives structured cancellation.
Three squads share checkout steps and each forked a page object. What do you introduce?
The strongest answer: A shared task / actor layer for the jobs they all perform, versioned like a library, with a contract for the few page APIs underneath
Why it works: Exactly. Shared jobs want a shared language. Ownership of a library beats a turf war.
- Shared tasks for shared jobs.
- Version the library.
- Do not win by turf.
- A new tool is not a shared layer.
How do you compose several transformation stages over a huge stream without buffering?
The strongest answer: Chain generators so each stage yields to the next — nothing is materialised, memory stays constant, and any stage can stop the pipeline early
Why it works: Right. Composition without materialisation is what generators are for.
- Generators chain without buffering.
- Memory stays constant across stages.
- Early exit stops upstream work.
- itertools composes these cheaply.
Which boundaries do you treat as famous, not optional?
The strongest answer: Money (0, 0.01, rounding), dates (DST, month-end, leap), and empty collections — I do not only test “Tuesday with one item”
Why it works: Exactly. Money, time, empty. Those are where products bleed.
- Money edges.
- Calendar edges.
- Empty lists.
- Do not trust the framework to care.
A provider broke a field the mobile app needs. Where should that have failed?
The strongest answer: On the provider pipeline, verifying consumer contracts, before the merge — not in the app store a week later
Why it works: Exactly. The provider gate is the cheap place. Store ratings are the expensive place.
- Fail on the provider PR.
- Contracts before the store.
- UI is a late witness.
- Breaking changes need a version, not a shrug.
A gesture-heavy Android suite is flaky: taps land on a view that is still sliding into place.
What do you change, and what does it cost you?
The strongest answer: Turn the device animation scales off for the run — developer settings or the driver capability for it — and still wait for the element to be ready; the cost is that nothing tests the animation, so keep one test that does
Why it works: Right — remove the cause, keep the readiness check, and be honest about the fidelity you dropped.
- Remove the animation, not the symptom.
- Turn animation scales off for the run.
- Still wait for the element to be ready.
- Name the fidelity you gave up.
5+ years
You are asked for a two-year automation strategy. What are the first three pages?
The strongest answer: The risks you owe, the paved road (data, waits, CI trains), and the gates — not a tool bake-off and not a headcount chart without outcomes
Why it works: Exactly. Risks, road, gates. Tools and reporting lines are appendices.
- Name the risks you owe.
- Describe the paved road.
- Write the gates.
- Put tools in the back.
Six squads share your framework, and every triage starts with somebody rerunning the test locally with extra logging turned on.
What must every run leave behind so that stops?
The strongest answer: A structured log per test, written by default: the steps taken, the requests and responses with secrets redacted, the data the test created, and the build and environment — attached to the test, not buried in one console stream
Why it works: Right — triage should read the run, not reproduce it, and default-on is the only setting that helps.
- Capture it on the first run.
- Structured log per test, by default.
- Redact secrets; keep the requests.
- Attach it to the test, not a stream.
You are designing a Python service that fans out to many slow downstreams. What shapes the design?
The strongest answer: Async end to end so waiting costs a task rather than a thread, with bounded concurrency per downstream, timeouts on every call and a budget for the whole request
Why it works: Right, and the bound per downstream is what stops you from becoming their outage.
- Async suits many concurrent waits.
- Bound concurrency per dependency.
- Timeout every call, budget the request.
- Measure with real downstream latency.
How do you defend a thin UI pack to a board that wants “what users see”?
The strongest answer: Users see outcomes; most bugs are cheaper in API and unit — I show late bugs we moved down and the minutes we gave back, not a lecture about ice-cream cones
Why it works: Exactly. Show converted bugs and time returned. Contempt and hiding lose the room.
- Show bugs moved down.
- Show minutes returned.
- Keep a thin honest UI pack.
- Do not insult or hide.
A test creates a record through the write API and immediately reads it back from a list endpoint served by a read replica. It passes locally and fails in staging perhaps one run in ten.
What is going on, and what do you do?
The strongest answer: Replication lag — the read replica has not caught up. The test should poll the read path until the record appears, within a bounded window, and the real question for the team is whether users are promised read-your-writes on that screen
Why it works: Right — and turning it into a product question is what makes it a fix rather than a workaround.
- Eventual consistency is not flake.
- Poll the read path, with a bound.
- Ask what the product promises.
- Do not test a path users never take.
Your worker cannot keep up with the producer and memory climbs. What is the right answer?
The strongest answer: Backpressure — a bounded queue that makes the producer wait or shed load, so the system degrades predictably instead of consuming memory until it dies
Why it works: Exactly. A bounded queue turns an outage into a slowdown you can see.
- Bounded queues create backpressure.
- Unbounded queues move the failure to memory.
- Decide explicitly: wait, shed or drop.
- Make the decision observable.
You are writing the rule that decides whether a new test runs on every pull request or only nightly. What is the rule?
The strongest answer: On the pull request if it is fast, deterministic and covers something we would block a merge for; nightly for the slow, the broad matrix and anything whose failure needs a person — and every nightly failure gets an owner by morning or the test leaves the pack
Why it works: Right — and the ownership clause is what stops nightly becoming a graveyard nobody reads.
- Block a merge only for blocking risk.
- Slow and broad runs on a train.
- Every nightly failure gets an owner.
- Never demote a test to hide a number.
Rerunning red PRs is how the company ships. What do you change first?
The strongest answer: Make the tax visible, freeze new UI tests that do not meet the wait/data bar, and staff a flake week with owners — culture moves when the cost is on a page
Why it works: Exactly. Visibility, a bar, and named work. Speeches and new toys do not retire a habit.
- Put the tax on a page.
- Hold a bar for new tests.
- Staff named flake work.
- Do not tool-swap the culture.
A Python service does heavy numeric work per request and latency is unacceptable. What options do you weigh?
The strongest answer: Push the loop into a library that releases the GIL, move the work to a process pool or a separate service, precompute or cache, or accept that this part does not belong in Python
Why it works: Right — and knowing when to leave Python for one component is part of the answer.
- Vectorised or native code releases the GIL.
- Process pools give real parallelism.
- Precompute or cache where the input repeats.
- Some components should leave Python.
You are starting mobile automation for a product with separate native Android and iOS apps over one shared backend.
One Appium suite for both platforms, or Espresso and XCUITest per platform?
The strongest answer: It turns on how much the two apps share. Same flows, one Appium suite keeps the journeys in one place; diverged teams and UIs, native suites run faster and closer to the code. Either way, shared rules are tested once at the API
Why it works: Right — the answer turns on shared flows and team shape, and the backend carries the rules either way.
- Shared flows favour one suite.
- Diverged apps favour native runners.
- Test shared rules once, at the API.
- Own the choice, and say why.
What does a senior SDET owe that a strong junior does not yet?
The strongest answer: A paved road other people can use — data, lints, CI trains, coaching — not a private suite that only they can debug
Why it works: Exactly. Senior is leverage. A hero suite is a junior skill at larger volume.
- Build the road others use.
- Coach and lint.
- Do not worship volume.
- Do not vanish into review-only.
Under load your async service exhausts database connections. What went wrong?
The strongest answer: Concurrency was unbounded, so every in-flight request took a connection — bound the pool and the concurrency together, keep remote calls out of transactions, and queue rather than open more
Why it works: Exactly. Async makes it trivial to start more work than your resources support.
- Async removes the natural limit on concurrency.
- Size the pool and bound in-flight work.
- Never hold a transaction across a network call.
- Queue instead of opening more connections.