Skip to content

Infosys Interview Prep β€” SDET / QA Automation (DEEP DIVE, June 2026)

Single self-contained study doc for a 3-day crunch. Topics: Python, Selenium, Rest Assured, plus the core CS / SQL / framework / project / behavioral material Infosys tests. Each concept is written as: a speakable answer + code + the "why"/trade-off + a likely follow-up. If you read only this file, you can walk in prepared; dip into 05/09/13/15/18 only for rare deep follow-ups.

⚠️ Language reality: Rest Assured is Java-only; the B2B Selenium framework is Java; Python is the language + pytest (AI-eval). So prep Python as a language, Selenium in both, Rest Assured in Java.


0. Infosys Interview Pattern (2026)

Typical lateral SDET/QA flow (varies by band β€” JL4/JL5):

Round Tests How to win
Tech Round 1 Core language (Python/Java), Selenium, API/Rest Assured, OOPs, collections, framework Crisp 30-sec answer β†’ "want me to go deeper?"
Tech Round 2 / Managerial Project deep-dive, framework design, real-time scenarios, debugging, SQL Drive the story; quote a metric; admit trade-offs
HR Behavioral, stability, notice, comp Calm, honest, positive about Questt/Avysh

Interviewer style: fundamentals + clarity over jargon; heavy on "explain your framework" and "give me a real scenario"; they probe why; usually one medium coding question; SQL almost guaranteed.

Golden rules for every answer: structure it β†’ point β†’ example β†’ trade-off; quote one real metric; for each project have one "what I'd improve." Never "I don't know" β†’ "I haven't used X directly, but conceptually…".


1. PYTHON β€” Deep Dive (language + pytest + coding)

Role tests Python as the language. This section is study-sufficient: speak the crisp answer, write the snippet, state the trade-off, anticipate the follow-up.


1.1 Data types & mutability

Speakable answer: "Python has immutable types β€” int, float, bool, str, tuple, frozenset, bytes β€” and mutable ones β€” list, dict, set, bytearray. Immutable means any 'modification' returns a new object; the original is untouched. Mutability matters for three reasons: only immutable (hashable) objects can be dict keys / set members; passing a mutable object into a function lets the function mutate the caller's data; and immutables are safe to share."

s = "hello"; s.upper()      # returns "HELLO"; s is still "hello"
lst = [1, 2]; lst.append(3) # mutates in place -> [1, 2, 3]
t = (1, 2); t[0] = 9        # TypeError β€” tuples are immutable
Type Mutable? Hashable? (usable as dict key)
int, float, str, bool, tuple, frozenset No Yes
list, dict, set Yes No

Why/trade-off: Immutability buys safety and hashability; mutability buys in-place efficiency. The classic bug is the mutable default argument (see 1.18).

Follow-up β€” "Is a tuple always hashable?" Only if all its elements are hashable; (1, [2]) is not.


1.2 is vs == (and interning)

Speakable answer: "== compares value (calls __eq__); is compares identity (same object in memory, same id()). Use == for equality and is only for singletons like None, True, False."

a = [1, 2]; b = [1, 2]; c = a
a == b   # True  (same contents)
a is b   # False (different objects)
a is c   # True  (same object)
x is None    # correct idiom; never `x == None`

Interning gotcha:

a = 256; b = 256; a is b      # True  β€” CPython caches small ints [-5, 256]
a = 257; b = 257; a is b      # False β€” outside cache (often; impl detail)
Short string literals are also interned, so "hi" is "hi" is often True β€” never rely on this; it's an implementation optimization, not a guarantee.

Follow-up β€” "Why does x == None work but is discouraged?" A class can override __eq__ and lie; identity (is None) can't be faked and is faster.


1.3 Strings (methods, f-strings, slicing)

Strings are immutable sequences of Unicode code points.

s = "Rohan Bhavsar"
len(s)                 # 13
s.upper(); s.lower(); s.title(); s.swapcase()
s.strip(); s.lstrip(); s.rstrip()
s.split()              # ['Rohan', 'Bhavsar']  (split on whitespace runs)
",".join(["a", "b"])   # "a,b"
s.replace("o", "0")
s.startswith("Ro"); s.endswith("ar"); "han" in s
s.find("a")            # index or -1;  s.index("a") raises if absent
s.count("a")
"42".isdigit(); "abc".isalpha(); "a1".isalnum()

f-strings (preferred):

name, n = "Rohan", 3.14159
f"{name} -> {n:.2f}"      # 'Rohan -> 3.14'
f"{n=}"                    # 'n=3.14159'  (debug form, 3.8+)
f"{42:>6}"  f"{42:06}"     # padding / zero-fill

Slicing β€” s[start:stop:step] (stop exclusive):

s = "abcdef"
s[1:4]    # 'bcd'
s[::-1]   # 'fedcba'  (reverse)
s[::2]    # 'ace'
s[-3:]    # 'def'

Follow-up β€” "Why is s += x in a loop slow?" Each += creates a new string (immutable), so it's O(nΒ²). Build a list and "".join(...) it β€” O(n).


1.4 Lists, dicts, sets + comprehensions

# list
lst = [1, 2, 3]; lst.append(4); lst.insert(0, 0); lst.pop(); lst.remove(2)
lst[1:3]; sorted(lst); lst.sort(reverse=True)

# dict
d = {"a": 1}; d["b"] = 2; d.get("z", 0)   # 0 if missing (no KeyError)
d.keys(); d.values(); d.items(); d.setdefault("c", []); "a" in d

# set β€” unique, O(1) membership
s = {1, 2, 3}; s.add(4); s.discard(9)      # discard: no error if absent
{1,2,3} & {2,3,4}   # &=∩ {2,3}, |=βˆͺ, -=diff, ^=symmetric diff

Comprehensions (Pythonic transform + filter):

[n*n for n in range(5)]                 # list
[n for n in range(10) if n % 2 == 0]    # with filter
{n: n*n for n in range(3)}              # dict comp
{c for c in "banana"}                   # set comp  -> {'b','a','n'}
[(x, y) for x in (1,2) for y in (3,4)]  # nested

Follow-up β€” "Comprehension vs map/filter?" Comprehensions are more readable for transform+filter; map(int, items) is fine when applying an existing function. Comprehension materializes a full list β€” use a generator expression (...) for large data.


1.5 Generators & yield (lazy / memory-efficient)

Speakable answer: "A generator is a function with yield. Calling it returns a lazy iterator that produces values one at a time and pauses between them, so it uses O(1) memory regardless of how much it eventually yields. Perfect for streaming large files or infinite sequences."

def counter(n):
    i = 0
    while i < n:
        yield i      # pause here; resume on next()
        i += 1

g = counter(3)
next(g)              # 0
list(counter(3))     # [0, 1, 2]

# Stream a 10 GB file without loading it all:
def error_lines(path):
    with open(path) as f:
        for line in f:           # file objects are already lazy iterators
            if "ERROR" in line:
                yield line.strip()

Generator expression (parens, not brackets) β€” lazy sibling of a list comp:

total = sum(n*n for n in range(10**6))   # O(1) memory, no giant list

List comprehension Generator
Memory O(n) β€” all at once O(1) β€” one at a time
Reusable? Yes (it's a list) No β€” exhausted after one pass

Follow-up β€” "yield vs return?" return ends the function with one value; yield pauses and can resume, producing a stream.


1.6 Decorators (with @retry)

Speakable answer: "A decorator is a callable that takes a function and returns a wrapped function, to add behavior β€” logging, timing, caching, retries β€” without touching the original. @deco is sugar for f = deco(f). Use functools.wraps so the wrapper keeps the original __name__/__doc__."

from functools import wraps

def log_calls(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        print(f"calling {fn.__name__}")
        return fn(*args, **kwargs)
    return wrapper

@retry (decorator that takes arguments β€” three nested layers):

import time
from functools import wraps

def retry(times=3, delay=1, exceptions=(Exception,)):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            last = None
            for attempt in range(1, times + 1):
                try:
                    return fn(*args, **kwargs)
                except exceptions as e:
                    last = e
                    print(f"attempt {attempt} failed: {e}")
                    time.sleep(delay)
            raise last
        return wrapper
    return decorator

@retry(times=3, delay=2, exceptions=(ConnectionError,))
def call_api():
    ...

Why/trade-off: Cross-cutting concerns stay out of business logic. Common real ones: functools.lru_cache (memoization), @property, @staticmethod, pytest fixtures, Flask routes.

Follow-up β€” "Why @wraps?" Without it the decorated function reports the wrapper's name/docstring, breaking introspection, debugging, and some frameworks.


1.7 Context managers (with, __enter__/__exit__)

Speakable answer: "A context manager guarantees setup and teardown around a block via with. The object implements __enter__ (runs on entry, return value is bound to as) and __exit__ (runs on exit, even if an exception is raised). Most common use is files β€” with open(...) auto-closes."

with open("f.txt") as f:     # __exit__ closes f even on exception
    data = f.read()

Custom class-based:

import time
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"{time.time() - self.start:.3f}s")
        return False   # False -> propagate any exception; True -> suppress it

Easier with @contextmanager (yield-based):

from contextlib import contextmanager
@contextmanager
def timer():
    start = time.time()
    try:
        yield                 # code inside `with` runs here
    finally:
        print(f"{time.time() - start:.3f}s")

Follow-up β€” "What if __exit__ returns True?" It suppresses the exception β€” usually a bad idea; do it only intentionally.


1.8 OOP β€” classes, self/cls, method types, inheritance, MRO/super, dunders

class Animal:
    species = "generic"                 # class attribute (shared)

    def __init__(self, name):           # instance method; self = the instance
        self.name = name                # instance attribute

    def speak(self):
        return "..."

    @classmethod                         # gets the class, not the instance
    def of_unknown(cls):
        return cls("Unknown")            # cls enables subclass-correct creation

    @staticmethod                        # no self/cls β€” just namespaced function
    def is_animal(obj):
        return isinstance(obj, Animal)

class Dog(Animal):
    species = "dog"
    def speak(self):                     # override
        return super().speak() + " Woof"  # super() = call parent
Method First arg Use
instance method self needs instance state
@classmethod cls alternative constructors / factory (cls(...))
@staticmethod β€” utility logically grouped in the class

MRO (Method Resolution Order) β€” for multiple inheritance, Python uses C3 linearization (left-to-right, depth-first, deduped):

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
D.__mro__   # (D, B, C, A, object)  -> super() walks this order

Key dunders: __init__ (construct), __str__ (user-readable, print), __repr__ (dev/debug, REPL β€” define this one if you pick one), __eq__ + __hash__ (pair them; equal objects must hash equal), __lt__ (sorting), __len__, __getitem__, __call__, __enter__/__exit__.

Follow-up β€” "__str__ vs __repr__?" __str__ for end users; __repr__ unambiguous for developers and the fallback when __str__ is missing. Follow-up β€” "__new__ vs __init__?" __new__ creates the object, __init__ initializes it; you rarely override __new__ (singletons, immutables).


1.9 *args / **kwargs

Speakable answer: "*args collects extra positional args into a tuple; **kwargs collects extra keyword args into a dict. In a call, */** unpack. Signature order is: positional, *args, keyword-only, **kwargs."

def f(*args, **kwargs):
    print(args, kwargs)
f(1, 2, x=3)             # (1, 2) {'x': 3}

nums = [1, 2, 3]; print(*nums)          # unpack -> print(1, 2, 3)
cfg = {"sep": "-"};  print(1, 2, **cfg) # unpack kwargs

def g(a, *args, b, **kwargs): ...        # b is keyword-only (after *args)

Follow-up β€” "Forward args to a wrapped function?" return original(*args, **kwargs) β€” the universal passthrough (used in every decorator).


1.10 Exceptions (try/except/else/finally, custom)

class ValidationError(Exception):        # custom β€” subclass Exception
    pass

try:
    risky()
except (ValueError, KeyError) as e:      # catch specific types
    log(e)
except ValidationError as e:
    raise                                 # re-raise
else:
    print("ran only if no exception")     # success-only code
finally:
    cleanup()                             # ALWAYS runs (cleanup)

Speakable answer: "except handles, else runs only when no exception fired (keep the try block minimal), finally always runs for cleanup. Catch the narrowest exception type; bare except: even swallows KeyboardInterrupt. Define custom exceptions by subclassing Exception for domain-specific errors."

Follow-up β€” "raise vs raise from?" raise NewErr() from e preserves the original cause in the traceback (exception chaining).


1.11 File handling

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()          # whole file
    # f.readline() / f.readlines() / iterate line-by-line (lazy, preferred)

with open("out.txt", "w") as f:     # 'w' truncate, 'a' append, 'r+' read+write
    f.write("hello\n")
    f.writelines(["a\n", "b\n"])

import json
with open("cfg.json") as f:
    cfg = json.load(f)               # file -> dict; json.loads for a string
with open("cfg.json", "w") as f:
    json.dump(cfg, f, indent=2)

Always use with so the file closes even on exception. Iterate the file object directly to stream large files without loading them fully.

Follow-up β€” "Text vs binary mode?" "r"/"w" decode/encode text via encoding; "rb"/"wb" give raw bytes (images, etc.).


1.12 Iterators vs iterables

Term Has Example
Iterable __iter__() returning an iterator list, dict, set, str
Iterator __next__() (and returns itself from __iter__) result of iter(lst)
Generator special, simpler iterator built with yield (x for x in ...)

lst = [1, 2, 3]      # iterable
it = iter(lst)       # iterator
next(it)             # 1 ... eventually raises StopIteration
A for loop works on any iterable: it calls iter() then next() until StopIteration.

Follow-up β€” "Difference between iterable and iterator?" An iterable can produce a fresh iterator each time; an iterator is single-use and tracks position.


1.13 Shallow vs deep copy

import copy
original = [[1, 2], [3, 4]]

shallow = copy.copy(original)        # or original[:] or list(original)
shallow[0].append(99)
original                              # [[1, 2, 99], [3, 4]]  inner list SHARED!

deep = copy.deepcopy(original)        # recursively independent
deep[0].append(100)
original                              # unchanged

Speakable answer: "Shallow copy makes a new outer container but shares the inner objects; deep copy recursively duplicates everything. Use shallow when contents are immutable; deep when you need full independence of nested mutables."

Follow-up β€” "Is original[:] a deep copy?" No β€” it's shallow.


1.14 GIL (interview-level)

Speakable answer: "The GIL is a CPython mutex that lets only one thread execute Python bytecode at a time. So threads don't speed up CPU-bound work β€” use multiprocessing (each process has its own interpreter + GIL) for true parallelism. But the GIL is released during I/O, so threads (or asyncio) are great for I/O-bound work like API calls or DB queries."

Workload Best tool Why
CPU-bound multiprocessing bypasses GIL with separate processes
I/O-bound threading / asyncio GIL released during I/O waits

Follow-up β€” "Is the GIL going away?" PEP 703 free-threaded build is experimental in 3.13+; for interviews, assume CPython has the GIL. Wrong answer that rejects you: "Threading doesn't work in Python." It works well for I/O β€” just not CPU-bound work.


1.15–1.18 High-frequency gotchas (rapid fire)

Truthiness: falsy = False, 0, 0.0, "", [], {}, set(), None. Everything else truthy.

enumerate / zip:

for i, v in enumerate(items, start=1): ...
for a, b in zip(list1, list2): ...

Mutable default argument trap (1.18):

def add(item, lst=[]):       # BUG: default list created ONCE, shared across calls
    lst.append(item); return lst
add(1); add(2)               # [1] then [1, 2] β€” surprise!

def add(item, lst=None):     # FIX
    if lst is None: lst = []
    lst.append(item); return lst
Why: default values are evaluated once at definition time, not per call.


pytest (the SDET-critical part)

P.1 Fixtures + scopes

A fixture is a @pytest.fixture function that provides setup (and optional teardown). Tests request it by parameter name; pytest injects it (dependency injection).

import pytest

@pytest.fixture
def sample_user():
    return {"id": 1, "email": "r@x.com"}

def test_email(sample_user):                 # requested by name
    assert "@" in sample_user["email"]

yield for teardown β€” everything after yield runs after the test, even on failure:

@pytest.fixture
def db_conn():
    conn = connect()      # setup
    yield conn            # value handed to test
    conn.close()          # teardown

Scopes (how often the fixture is created):

Scope Created once per Use for
function (default) each test fresh state
class each test class class helpers
module each .py file module seed
session whole run DB pool, browser launch

@pytest.fixture(scope="session")
def engine(): ...
A smaller scope (function) may request a larger scope (session); the reverse raises ScopeMismatch. autouse=True runs a fixture without being requested β€” use sparingly (hidden side effects).

P.2 conftest.py

Put shared fixtures in conftest.py; pytest discovers them automatically by directory tree walk (a test sees fixtures in its file + every conftest.py up the tree). You do not import fixtures β€” that's the classic wrong answer.

P.3 parametrize (data-driven)

@pytest.mark.parametrize("email,expected", [
    ("a@x.com", True),
    ("invalid", False),
    ("", False),
], ids=["valid", "no-at", "empty"])
def test_validator(email, expected):
    assert is_valid_email(email) == expected
Stacking two @parametrize produces the cross-product. indirect=True routes a param through a fixture.

P.4 Markers

@pytest.mark.skip(reason="WIP")
@pytest.mark.skipif(sys.version_info < (3, 10), reason="needs 3.10+")
@pytest.mark.xfail(reason="known bug")
@pytest.mark.smoke                       # custom; declare in pytest.ini
Run by marker: pytest -m "smoke and not slow". Run by name substring: pytest -k login. Use --strict-markers to catch typos.

P.5 Useful plugins

Plugin Does
pytest-xdist parallel run (-n 4)
pytest-rerunfailures retry flaky (--reruns 2)
pytest-cov coverage
pytest-mock mocker fixture over unittest.mock
pytest-html HTML report
pytest-asyncio async tests

Follow-up β€” "yield vs try/finally in a fixture?" yield is idiomatic and cleaner; both work, but try/finally signals a unittest.TestCase mindset.


Coding Questions (clean Python solutions)

The ~12 highest-yield problems for a Python SDET screen. State complexity out loud.

C.1 Reverse a string β€” O(n) / O(n)

def reverse(s): return s[::-1]

def reverse_manual(s):            # if "no library" asked
    chars = list(s); l, r = 0, len(chars) - 1
    while l < r:
        chars[l], chars[r] = chars[r], chars[l]
        l, r = l + 1, r - 1
    return "".join(chars)

C.2 Palindrome check β€” O(n) / O(1)

def is_palindrome(s):                       # strict
    return s == s[::-1]

def is_palindrome_relaxed(s):               # alphanumeric, case-insensitive
    cleaned = [c.lower() for c in s if c.isalnum()]
    return cleaned == cleaned[::-1]

C.3 Anagram check β€” O(n) / O(1)

from collections import Counter
def is_anagram(a, b):
    return Counter(a) == Counter(b)         # sorted(a)==sorted(b) is O(n log n)

C.4 First non-repeating character β€” O(n) / O(1)

from collections import Counter
def first_unique(s):
    freq = Counter(s)
    return next((c for c in s if freq[c] == 1), None)

C.5 Word frequency / top-K β€” O(n)

from collections import Counter
def word_freq(text):
    return Counter(text.lower().split())

def top_k(text, k):
    return [w for w, _ in Counter(text.lower().split()).most_common(k)]

C.6 Two Sum β€” O(n) / O(n)

def two_sum(nums, target):
    seen = {}                                # value -> index
    for i, n in enumerate(nums):
        if target - n in seen:
            return [seen[target - n], i]
        seen[n] = i

C.7 Longest substring without repeating chars β€” O(n) (sliding window)

def longest_unique(s):
    seen = {}; l = best = 0
    for r, c in enumerate(s):
        if c in seen and seen[c] >= l:
            l = seen[c] + 1                  # jump left past the duplicate
        seen[c] = r
        best = max(best, r - l + 1)
    return best

C.8 Group anagrams β€” O(nΒ·k log k)

from collections import defaultdict
def group_anagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        groups["".join(sorted(s))].append(s)
    return list(groups.values())

C.9 FizzBuzz β€” O(n) (warm-up; they do ask)

def fizzbuzz(n):
    out = []
    for i in range(1, n + 1):
        if i % 15 == 0:   out.append("FizzBuzz")
        elif i % 3 == 0:  out.append("Fizz")
        elif i % 5 == 0:  out.append("Buzz")
        else:             out.append(str(i))
    return out

C.10 Fibonacci β€” iterative O(n)/O(1); memoized recursion

def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

from functools import lru_cache
@lru_cache(maxsize=None)                     # mention this for "optimize recursion"
def fib_rec(n):
    return n if n < 2 else fib_rec(n-1) + fib_rec(n-2)

C.11 Email validator β€” O(n) (the SDET favorite)

import re
def is_valid_email(email):
    pattern = r'^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
    return bool(re.match(pattern, email or ""))
Say: "Simplified, not full RFC-5322 β€” fine for test validation; production needs a longer regex or a library."

C.12 Luhn credit-card check β€” O(n) / O(1)

def is_valid_luhn(card):
    s = (card or "").replace(" ", "")
    if not s.isdigit() or not (13 <= len(s) <= 19):
        return False
    total = 0
    for i, ch in enumerate(reversed(s)):
        n = int(ch)
        if i % 2 == 1:                        # double every 2nd digit from right
            n *= 2
            if n > 9: n -= 9
        total += n
    return total % 10 == 0

Bonus C.13 Missing number in 1..N β€” O(n) / O(1)

def missing_number(arr, n):
    return n * (n + 1) // 2 - sum(arr)        # expected sum minus actual

Bonus C.14 LRU Cache β€” O(1) ops

from collections import OrderedDict
class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.cache = OrderedDict()
    def get(self, key):
        if key not in self.cache: return -1
        self.cache.move_to_end(key)           # mark most-recently used
        return self.cache[key]
    def put(self, key, value):
        if key in self.cache: self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.cap:
            self.cache.popitem(last=False)    # evict least-recently used

Complexity cheat: dict/set lookup, insert, delete = O(1) avg β€’ list index = O(1), insert/delete at front = O(n), membership in = O(n) β€’ sorted() = O(n log n) β€’ slicing s[a:b] = O(bβˆ’a). When asked "optimize," reach for a hash map (Counter/dict/set) or sliding window first.

2. SELENIUM β€” Deep Dive

Interview language is Python (examples primary). Java notes added where the candidate's B2B framework (Java + Selenium 4 + TestNG + REST Assured + ExtentReports) is the natural reference. The single most-asked cluster: the three waits, locators (XPath vs CSS), exceptions and their fixes, and POM.


2.1 Architecture & the W3C WebDriver protocol

Speakable answer: "Selenium is five layers. My test code calls the client library (the Python selenium bindings). The client serializes each command into the W3C WebDriver protocol β€” HTTP + JSON β€” and sends it to a local browser driver like chromedriver. The driver controls the actual browser through its native automation API and returns the result. The key insight: every command is a separate HTTP round-trip, so driver.find_element(...).click() is two requests. That per-request overhead is the architectural reason Selenium feels slower than Playwright's persistent WebSocket."

Test code (Python / Java / JS / C#)
   ↓ client library
W3C WebDriver protocol (HTTP + JSON)
   ↓
Browser driver (chromedriver / geckodriver / msedgedriver)
   ↓
Browser

Session lifecycle: webdriver.Chrome() β†’ client launches chromedriver as a local HTTP server (port 9515) β†’ POST /session with capabilities β†’ driver launches Chrome and returns a session id β†’ every later command is an HTTP request carrying that session id.

What Selenium 4 changed (memorize β€” top question):

Change What it gives you
W3C WebDriver protocol (replaces JSON Wire) Standardized, more reliable, slightly faster
Relative locators (above/below/near/to_left_of/to_right_of) Position-aware finding
Native CDP in Chrome/Edge Network throttle, geolocation mock, request capture, console
switch_to.new_window("tab") Opens and switches β€” no handle dance
Browser-specific Options Type-safe config (replaces loose DesiredCapabilities)
Selenium Manager Auto-downloads matching driver β€” no version-pinning hell

W3C vs old JSON Wire Protocol:

JSON Wire (Selenium 3) W3C WebDriver (Selenium 4)
Standardization Selenium-only spec W3C standard; browsers implement natively
Reliability Encoding mismatches across drivers Well-defined wire format, consistent
Capabilities DesiredCapabilities (loose) Browser-specific Options (type-safe)
Status Removed Default and only protocol

Why / trade-off: standardized protocol = fewer cross-browser quirks and slightly faster, but the wire is still HTTP request-per-command β€” inherent latency vs WebSocket tools.

Follow-up β€” "Why is each command an HTTP request a problem?" β†’ At ~200 commands/test the round-trip overhead adds up to real seconds; it's why chatty tests are slow and why you minimize redundant finds.


2.2 Selenium Manager & driver setup

Speakable answer: "Selenium 4.6+ ships Selenium Manager β€” it auto-resolves and downloads the matching driver binary. So webdriver.Chrome() with no path just works. That killed an entire class of CI failures we used to debug weekly from chromedriver version drift."

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

opts = Options()
opts.add_argument("--headless=new")          # Chrome 109+ headless
opts.add_argument("--no-sandbox")            # Docker-as-root workaround
opts.add_argument("--disable-dev-shm-usage") # /dev/shm is 64MB in Docker
opts.add_argument("--disable-gpu")           # legacy Windows headless fix
opts.add_argument("--window-size=1920,1080") # deterministic viewport for screenshots
driver = webdriver.Chrome(options=opts)      # Selenium Manager finds the driver

Java note: legacy frameworks set System.setProperty("webdriver.chrome.driver", path) and shipped per-OS binaries in resources/driver/{os}/. Modern Java just calls new ChromeDriver() (Selenium Manager) or WebDriverManager.chromedriver().setup().

The CI headless quartet β€” know what each flag does: --no-sandbox (root in Docker), --disable-dev-shm-usage (tiny /dev/shm), --disable-gpu (legacy Windows crash), --window-size (screenshot fidelity). --disable-blink-features=AutomationControlled hides the automation flag from bot detection.

Follow-up β€” "Why do tests pass headed but fail headless?" β†’ Different default viewport (set --window-size), no GPU (canvas/WebGL differs), missing system fonts in the Docker image, and bot detection that fires only on headless.


2.3 The eight locators + priority

from selenium.webdriver.common.by import By

driver.find_element(By.ID, "email")                      # fastest — browser keeps id→element map
driver.find_element(By.NAME, "password")                 # form fields
driver.find_element(By.CLASS_NAME, "btn-primary")        # single CSS class
driver.find_element(By.TAG_NAME, "input")                # when one tag matches
driver.find_element(By.LINK_TEXT, "Sign in")             # exact anchor text
driver.find_element(By.PARTIAL_LINK_TEXT, "Sign")        # substring anchor text
driver.find_element(By.CSS_SELECTOR, "input[name='email']")
driver.find_element(By.XPATH, "//input[@id='email']")    # last resort

Priority I follow: ID β†’ Name β†’ CSS selector β†’ Link text (anchors only) β†’ XPath (last resort, but unavoidable for text-based matching or tree navigation).

find_element vs find_elements: find_element returns first match, throws NoSuchElementException if none. find_elements returns a list (empty, no exception) β€” use it for counting, iterating, or asserting absence.

Follow-up β€” "Why is ID fastest?" β†’ Browsers maintain an internal id β†’ element hash map, so lookup is O(1) and doesn't traverse the DOM.


2.4 CSS vs XPath β€” when each

Use CSS when Use XPath when
Simple attribute / class / descendant Need text matching: contains(text(),'Log')
Speed and readability matter Navigate up the tree: ancestor::form
Most everyday locators Navigate to siblings: following-sibling::input
Index/position logic: [last()], [1]

XPath cheatsheet (high-yield):

//input[@type='text']                                 attribute equality
//button[text()='Login']                              exact text
//button[contains(text(),'Log')]                      partial text
//div[contains(@class,'btn-primary')]                 partial class (@class is the whole string!)
//a[starts-with(@href,'/user')]                        starts-with
//div[@id='form']//input                              descendant (any depth)
//div[@id='form']/input                               direct child
//input[last()]                                       last match
//tr[td[contains(.,'Pending')]]                       row whose td contains text
//label[normalize-space()='Email']/following-sibling::input   sibling nav
//input[@id='email']/ancestor::form                   walk up the tree

Axes worth naming: ancestor, descendant, following-sibling, preceding-sibling, parent, child. Never use absolute XPath (/html/body/div[1]/...) β€” breaks on the first DOM change.

Speakable answer β€” "XPath vs CSS?" β†’ "CSS first for simple attribute or descendant locators β€” faster and more readable. XPath when I need text-based matching, walking up to an ancestor, or sibling navigation. CSS can't do contains(text()) or go up the tree, which is XPath's whole reason to exist. I never write absolute XPaths."

Selenium 4 relative locators (Python):

from selenium.webdriver.support.relative_locator import locate_with

email_label = driver.find_element(By.ID, "email-label")
email_input = driver.find_element(locate_with(By.TAG_NAME, "input").to_right_of(email_label))
# relations: above, below, to_left_of, to_right_of, near
When: forms where label↔input aren't formally associated. Trade-off: it's a rendered-position check, so it's slower and breaks on layout reflows β€” not a default.

Follow-up β€” "Absolute vs relative XPath?" β†’ Absolute starts at root and is brittle; relative starts with // and matches anywhere β€” always use relative with attribute filters.


2.5 The THREE waits

This is the single most-asked Selenium topic.

Implicit Explicit Fluent
Scope Global, every find_element One element, one condition One element + custom polling
Set Once on driver Per call (WebDriverWait) Per call (WebDriverWait + args)
Polling Driver-internal 500ms default Custom interval
Ignore exceptions No No (just times out) Yes β€” list of ignored types
Throws nothing extra TimeoutException TimeoutException
Use in prod Set to 0 95% of the time Transient staleness / custom condition
# IMPLICIT β€” global, set once (recommended: leave at 0)
driver.implicitly_wait(0)

# EXPLICIT β€” targeted condition (the one you should use)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
wait.until(EC.element_to_be_clickable((By.ID, "submit")))
wait.until(EC.visibility_of_element_located((By.ID, "status")))
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".spinner")))

# FLUENT β€” WebDriverWait with custom poll + ignored exceptions
from selenium.common.exceptions import StaleElementReferenceException
wait = WebDriverWait(driver, 30, poll_frequency=2,
                     ignored_exceptions=[StaleElementReferenceException])
el = wait.until(lambda d: d.find_element(By.ID, "data"))

# Custom lambda condition (e.g. wait for a list to fill)
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ".row")) == 10)

Java note: explicit waits use new WebDriverWait(driver, Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(...)); fluent is new FluentWait<>(driver).withTimeout(...).pollingEvery(...).ignoring(...).

Common ExpectedConditions:

Condition (Python EC.) Use when
visibility_of_element_located(by) Visible AND re-resolves locator each poll
element_to_be_clickable(by) Visible AND enabled
presence_of_element_located(by) In DOM (may be hidden)
invisibility_of_element_located(by) Spinner gone
text_to_be_present_in_element(by, txt) Element contains text
url_contains / title_contains After navigation
alert_is_present() Before switch_to.alert
staleness_of(elem) Wait for element to detach

The anti-pattern β€” mixing implicit + explicit: when both are active, waits can compound to the max of the two and behavior becomes implementation-specific. Rule: set implicit to 0, use explicit everywhere.

Why time.sleep is not a wait: it's a blind blocking pause. Too short β†’ flaky; too long β†’ slow. A wait polls a condition and returns the instant it's true. Replace time.sleep(2) with an explicit wait on the actual signal (a row appears, spinner disappears, text changes). For page readiness:

WebDriverWait(driver, 20).until(
    lambda d: d.execute_script("return document.readyState") == "complete")

Speakable answer β€” "Three waits, when each?" β†’ "Implicit is global and slows negative tests because every not-found check waits the full timeout. Explicit is targeted β€” WebDriverWait + element_to_be_clickable β€” what I use almost always. Fluent adds custom polling and ignored exceptions, useful when I expect transient staleness during a DOM refresh. Cardinal rule: never mix implicit and explicit β€” set implicit to zero."

Follow-up β€” "visibility_of vs visibility_of_element_located?" β†’ The former takes a WebElement you already hold and throws stale if the node is replaced; the latter takes a locator and re-resolves each poll, so it survives re-renders.


2.6 Interactions β€” click, sendKeys, dropdowns, Actions, upload

el = driver.find_element(By.ID, "email")
el.clear()
el.send_keys("a@x.com")
driver.find_element(By.ID, "submit").click()
el.get_attribute("value"); el.text

# Native <select> dropdown
from selenium.webdriver.support.select import Select
dd = Select(driver.find_element(By.ID, "country"))
dd.select_by_visible_text("India"); dd.select_by_value("IN"); dd.select_by_index(2)
dd.first_selected_option.text
# Select ONLY works on real <select>. Custom div dropdowns need click-open + click-option.

# ActionChains β€” hover, drag, key combos
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
a = ActionChains(driver)
a.move_to_element(menu).perform()                       # hover
a.context_click(el).perform()                           # right-click
a.double_click(el).perform()
a.drag_and_drop(src, target).perform()
a.click_and_hold(src).move_by_offset(100, 0).release().perform()   # HTML5 manual drag
a.key_down(Keys.CONTROL).click(link).key_up(Keys.CONTROL).perform()# Ctrl+click

# File upload β€” type the path into the hidden <input type=file>, no OS dialog
driver.find_element(By.ID, "upload").send_keys("/absolute/path/file.png")

clear() quirk: React/Angular controlled inputs may not fire input/change on clear(), so the form's dirty state doesn't update. Fallback:

el.send_keys(Keys.CONTROL, "a"); el.send_keys(Keys.DELETE)   # fires real key events

Follow-up β€” "Hover then click a submenu?" β†’ ActionChains(driver).move_to_element(menu_root).pause(0.5).click(submenu).perform() β€” the pause lets the CSS hover transition finish.


2.7 JavaScriptExecutor β€” uses & when not to

js = driver.execute_script
js("arguments[0].scrollIntoView({block:'center'});", el)   # scroll past sticky header
js("arguments[0].click();", el)                            # click when overlay intercepts
token = js("return localStorage.getItem('access_token');") # read browser storage
js("arguments[0].value = arguments[1];", inp, "hello")     # set value bypassing handlers
title = js("return document.title;")

Use: overlay intercepts a click; element needs real scrollIntoView; reading localStorage/sessionStorage/window state Selenium doesn't expose.

Don't use as a workaround for click failures β€” a JS click bypasses Selenium's actionability checks (visibility, enabled, hit-testing), so it can mask real bugs like "the button was disabled but the handler still fired."

Speakable answer: "Three legit uses β€” bypass an overlay-intercepted click, scroll-into-view with block:'center' past sticky headers, and read browser storage. I avoid JS-click as a default because it skips actionability checks and hides bugs."


2.8 Frames, windows/tabs, alerts, cookies

# iFrames β€” every locator inside searches only that frame
driver.switch_to.frame(0)                                  # by index
driver.switch_to.frame("paymentFrame")                     # by name/id
driver.switch_to.frame(driver.find_element(By.TAG_NAME, "iframe"))
driver.switch_to.parent_frame()                            # up one level
driver.switch_to.default_content()                         # back to top β€” DON'T FORGET

# Windows / tabs β€” old way
original = driver.current_window_handle
for h in driver.window_handles:
    if h != original:
        driver.switch_to.window(h); break
driver.close(); driver.switch_to.window(original)

# Selenium 4 native β€” opens AND switches, no handle juggling
driver.switch_to.new_window("tab")     # or "window"

# Alerts (browser-native popups)
alert = driver.switch_to.alert
alert.text; alert.accept(); alert.dismiss(); alert.send_keys("answer")

# Cookies β€” session reuse (poor man's storageState)
driver.add_cookie({"name": "session", "value": "abc123"})
cookies = driver.get_cookies()                 # save to JSON after login
driver.get("https://example.com")              # must be ON the domain first
for c in cookies: driver.add_cookie(c)         # then reload to reuse the session
driver.refresh()

Gotcha: forget default_content() after working in a frame and the next locator silently fails. Cookie reuse = log in once, save cookies, replay them to skip the login form (alternative: --user-data-dir persistent profile).

Follow-up β€” "Persist login across tests?" β†’ Save cookies after login and re-add them (must navigate to the domain first), or point Chrome at a persistent --user-data-dir profile.


2.9 Page Object Model + PageFactory + three-layer pattern

Plain POM (Python β€” store By locators, re-resolve on use):

class LoginPage:
    EMAIL = (By.ID, "email")
    PASSWORD = (By.ID, "password")
    LOGIN = (By.ID, "login")

    def __init__(self, driver): self.driver = driver

    def login(self, email, pwd):
        self.driver.find_element(*self.EMAIL).send_keys(email)
        self.driver.find_element(*self.PASSWORD).send_keys(pwd)
        self.driver.find_element(*self.LOGIN).click()

Java note β€” PageFactory @FindBy: PageFactory.initElements(driver, this) uses reflection to wire @FindBy-annotated fields to lazy proxy WebElements that re-resolve on each call (so they rarely go stale). Trade-off: a wrong locator surfaces as NoSuchElement only at use-time, not at construction.

@FindBy(xpath="//*[@view_id='orderAccept']/div/button") WebElement btnAccept;
public MyOrdersPage(WebDriver driver){ super(driver); PageFactory.initElements(driver, this); }

The candidate's three-layer pattern β€” Page + Helper + Test: - Page owns the DOM β€” locators + atomic actions. - Helper owns user journeys β€” select_brand(), create_region(), place_order(). - Test orchestrates helpers and asserts.

# PAGE: locators + atomic actions
# HELPER: business workflows
class RegionsHelper:
    def __init__(self, driver): self.page = RegionsPage(driver)
    def create_region(self, country, city):
        self.page.open_add_region(); self.page.set_country(country); self.page.set_city(city)
# TEST: reads like English
def test_create_region(regions_helper):
    regions_helper.create_region("India", "Pune")
    assert regions_helper.verify_region("Pune")

Base page pattern: pages extend a CommonUtils/BasePage holding shared robust helpers (click, send_keys_to, wait_for_clickable) β€” no per-page reinvention. A good click waits-for-clickable then catches stale and retries once.

Speakable answer β€” "Why both Page and Helper?" β†’ "Page owns the DOM, Helper owns the user journey, tests orchestrate. Tests read like English; a page-structure change only ripples to the Page class; helpers let tests share workflows. Worth it for a 40-page app, overkill for 5 pages."

POM anti-patterns: assertions inside POM (POM exposes state, tests assert); mega-POM (split per page); hardcoded test data (pass as params); returning raw WebElements; instantiating the driver inside the POM (receive it).

Follow-up β€” "POM vs PageFactory?" β†’ POM is the pattern (encapsulate elements + actions); PageFactory is Selenium's implementation helper using @FindBy + reflection with lazy proxies.


2.10 Common exceptions + FIXES

Exception Cause Fix
NoSuchElementException Locator didn't resolve Verify locator; add explicit wait; check you're in the right frame
StaleElementReferenceException Held reference to a detached DOM node (re-render) Re-find after the action; FluentWait that ignores stale; switch from WebElement refs to By locators
ElementClickInterceptedException Another element (overlay/sticky header) catches the click scroll_into_view({block:'center'}); close/wait-out the overlay; JS click as last resort
ElementNotInteractableException Element exists but hidden / disabled / not yet rendered Wait for element_to_be_clickable; check it's not zero-size or covered
TimeoutException Explicit wait condition never met Fix the condition/selector; increase timeout if genuinely slow
NoSuchFrameException / NoSuchWindowException Bad frame index/name or stale handle default_content() first; re-fetch handles
NoAlertPresentException switch_to.alert with no alert up Wait for alert_is_present() first
SessionNotCreatedException Driver/browser version mismatch Selenium Manager; align versions

Speakable answer β€” most common one: "StaleElementReference, by far. I hold a WebElement, the page re-renders that part of the DOM, and the reference points to a detached node. Three fixes by cause: if the re-render is predictable (after a Save reloads a list), re-fetch the element; for timing-driven re-renders, wrap in a FluentWait that ignores stale and retries; for constantly re-rendering pages, store By locators instead of element references so every access re-resolves."

Follow-up β€” "Debug a flaky test?" β†’ Run it 50Γ—; identify exception + line; replace any time.sleep/implicit wait with explicit on the real signal; check for cross-test shared state (make tests self-provision data); reproduce in CI conditions (headless, same parallelism); add onTestFailure/hook screenshot capture for evidence.


2.11 Selenium 4 extras β€” CDP & relative locators

Chrome DevTools Protocol (Python via execute_cdp_cmd / Selenium 4 BiDi):

driver.execute_cdp_cmd("Network.emulateNetworkConditions",
    {"offline": False, "latency": 100, "downloadThroughput": 10_000, "uploadThroughput": 5_000})
driver.execute_cdp_cmd("Emulation.setGeolocationOverride",
    {"latitude": 12.97, "longitude": 77.59, "accuracy": 1})   # Bangalore
driver.execute_cdp_cmd("Network.setBlockedURLs", {"urls": ["*.png", "*.jpg", "*.woff2"]})
driver.execute_cdp_cmd("Network.enable", {})
Uses: throttle network (simulate 3G), mock geolocation/timezone/user-agent, capture/assert network requests (no 4xx), block images to speed tests, listen to console for JS errors. Java uses the typed devTools.send(...) / devTools.addListener(...) API. Playwright has all of this first-class β€” a key reason it's preferred for greenfield.

Relative locators (to_right_of, above, below, near) β€” see 2.4.


2.12 Grid & parallel (brief)

Grid = one hub routes sessions to multiple nodes (browser/OS combos). Connect with webdriver.Remote(command_executor="http://hub:4444/wd/hub", options=opts). Modes: standalone (dev), hub-node (classic), distributed (scale), Docker. Docker tip: set shm_size: 2g on the Chrome node or it crashes; SE_NODE_MAX_SESSIONS caps concurrency per node. When: cross-browser/OS matrix and scaling beyond one machine (Safari needs Mac, etc.).

Parallel β€” Python: pytest -n 4 (pytest-xdist); make the driver a function/worker-scoped fixture so each worker gets its own browser β€” never share one driver across threads. Java note: TestNG parallel="classes" thread-count="4" + a ThreadLocal<WebDriver> so each thread sees its own driver (call .remove() in teardown to avoid leaks).

Follow-up β€” parallel="classes" vs "methods"? β†’ classes: each class in its own thread, methods serial within a class (safe when @BeforeMethod/@BeforeClass shares setup). methods: each @Test in its own thread β€” max parallelism but every test must be fully self-contained.


2.13 Scenario Q&A

Q1. Slow-loading dynamic content β€” how do you wait?

Explicit wait on a stable completion signal (first data row, success badge), not a spinner. If load is API-driven, wait on the CDP Network.responseReceived event. Never time.sleep as the primary wait.

Q2. Button is visible but click throws ElementClickIntercepted β€” fix?

Something overlays it (sticky header, modal, toast). scroll_into_view({block:'center'}), wait for the intercepting element to disappear, then click. JS click only as last resort because it skips actionability checks.

Q3. Test passes locally, fails in CI (headless) β€” why?

Different viewport (set --window-size=1920,1080), no GPU (canvas/WebGL), missing fonts in the Docker image, and bot detection on headless. Add --disable-blink-features=AutomationControlled; reproduce locally with the same headless flags.

Q4. Element keeps going stale on a live dashboard β€” strategy?

Stop holding WebElement references. Store By locators and re-resolve on every access, or use a FluentWait that ignores StaleElementReferenceException and retries the action.

Q5. File upload with no visible <input> (drag-drop uploader)?

If a hidden <input type=file> exists, send_keys the absolute path β€” works even when hidden. If there's truly no input, inject a File via JS and dispatch a drop event.

Q6. Open a link in a new tab and verify its content, then return?

Save current_window_handle, trigger the new tab (or switch_to.new_window("tab") in Selenium 4), switch to the new handle, assert, close(), switch back to the saved handle.

Q7. Persist login so you don't log in every test?

Log in once, get_cookies(), save to JSON. In later tests, navigate to the domain, re-add the cookies, refresh. Or use a persistent --user-data-dir Chrome profile.

Q8. A dropdown won't work with the Select class β€” why?

Select only handles native <select> elements. A custom div/list dropdown (e.g., a JS UI framework like Webix) needs click-to-open then click-the-option, often with an explicit wait on the option list.

Q9. Captcha blocks your flow β€” how do you handle it?

Never solve it. Ask devs for a test-mode bypass for whitelisted accounts; or use a known-valid test token; last resort, mock the captcha endpoint via CDP Fetch.requestPaused and return valid.

Q10. Two parallel tests interfere with each other β€” root cause?

Shared mutable state β€” a single static/global driver, or shared test data. Fix: per-worker driver (fixture/ThreadLocal), and each test provisions its own data (usually via API setup) so tests are independent.

Q11. Form input won't accept clear() (React) β€” fix?

clear() may not fire input/change. Use send_keys(Keys.CONTROL,"a") then send_keys(Keys.DELETE) to fire real keyboard events so the controlled component updates.

Q12. How do you take a screenshot on failure?

driver.save_screenshot(path) in a pytest pytest_runtest_makereport hook (or ITestListener.onTestFailure in TestNG) β€” attach to the report. Capture at the moment of failure, not end-of-test.

Q13. iframe interaction silently does nothing β€” what's wrong?

You're not inside the frame, or you switched in and never switched back. Every locator scopes to the current frame: switch_to.frame(...) to enter, work, then switch_to.default_content() before the next top-level locator.

Q14. Tests are slow β€” chatty Selenium. How do you speed them up?

Each command is an HTTP round-trip, so reduce them: cache By locators (not WebElement refs), avoid redundant finds, set implicit wait to 0 and use tight explicit waits, block images/fonts/analytics via CDP, run headless, and parallelize (pytest -n / TestNG parallel). For genuinely command-heavy suites, that's also the argument to move to Playwright's WebSocket model.

Q15. How would you build a Selenium framework from scratch today?

Python + pytest (or Java 17 + TestNG 7). Selenium Manager (no per-OS binaries). Function/worker-scoped driver fixture, no global driver. POM + Helper layer. Explicit waits only, implicit at 0, zero time.sleep. CSS-first locators, XPath only for text/tree navigation. CDP for network throttling/interception. Screenshots-on-failure via hook/listener. Docker Grid or pytest-xdist sharding in CI matrix per browser.

3. REST ASSURED & API TESTING β€” Deep Dive (Java)

3.1 Why API testing β€” the test-pyramid argument

Speakable answer: "Most regressions live in business logic, and business logic is exposed through APIs. So the API layer is where I catch the most bugs per unit of effort β€” faster, more stable, and cheaper to maintain than UI. In my last project the API suite caught roughly 70% of regressions before UI tests even ran, and that cut the PR pipeline from about 30 minutes of mostly-UI to under 10 minutes of mostly-API."

        /\
       /UI\        few   β€” slow (5-60s), brittle (80-95%), high maintenance
      /----\
     / API  \      many  β€” fast (100ms-1s), stable (95-99%), low-med maintenance
    /--------\
   /  Unit    \    most  β€” ms, 99%+ stable
  /------------\
Test type Run time Stability Maintenance
Unit ms 99%+ Low
API 100ms–1s 95–99% Low–Medium
UI/E2E 5–60s 80–95% High

Why API > UI for these: business-logic edge cases (UI masks invalid input, API still must reject it), authorization (UI hides the button, API must enforce), concurrency/race conditions, contract drift, and latency percentiles.

Follow-up β€” "If API tests cover so much, why have UI tests at all?" "UI tests verify presentation and the actual user flow β€” the thing the API can't see. I keep them few and high-value."


3.2 HTTP methods & idempotency

Speakable answer: "Idempotent means calling N times has the same effect as calling once. Safe means it doesn't change server state. GET is both. PUT and DELETE are idempotent but not safe. POST is neither. PATCH is usually idempotent but not guaranteed."

Method Purpose Idempotent? Safe?
GET Read Yes Yes
POST Create No No
PUT Replace whole resource Yes No
PATCH Partial update Sometimes No
DELETE Remove Yes No
HEAD Headers only Yes Yes
OPTIONS Allowed methods / CORS preflight Yes Yes

Why PATCH is "sometimes": PATCH {name:'Rohan'} is idempotent (set-style β€” twice still leaves name=Rohan). PATCH {counter: counter+1} (increment-style) is not β€” each call moves the value. The spec leaves it to the API designer.

PUT vs PATCH: PUT replaces the entire resource (unsent fields can be wiped); PATCH touches only the fields you send. PUT is strictly idempotent.

Idempotency-Key header (Stripe pattern): client sends a UUID; server caches the response so a retried POST is safe β€” important when networks make you retry a non-idempotent call.

Follow-up β€” "How do you test idempotency?" "Call the endpoint twice and assert the resource's state matches one call's effect β€” and for idempotency keys, that the second call returns the cached response without re-executing."


3.3 Status codes (with the traps)

Class Code Meaning / when
2xx 200 OK GET/PUT/PATCH with body
201 Created POST that creates (usually with Location header)
202 Accepted Async op queued
204 No Content DELETE / PUT with no body
4xx 400 Bad Request Malformed JSON, missing required field, syntactically broken
401 Unauthorized Missing/invalid credentials β€” "I don't know who you are"
403 Forbidden Authenticated but no permission β€” "I know you, you can't do this"
404 Not Found Resource ID doesn't exist (also used to hide existence cross-tenant)
405 Method Not Allowed POST on a GET-only endpoint
409 Conflict Duplicate email, version mismatch
415 Unsupported Media Type Wrong Content-Type
422 Unprocessable Entity Syntactically valid but fails business validation
429 Too Many Requests Rate limit
5xx 500 Internal Server Error Unhandled server exception
502 Bad Gateway Upstream returned bad response
503 Service Unavailable Server overloaded / down / maintenance
504 Gateway Timeout Upstream too slow

401 vs 403 β€” the classic trap: 401 = unknown identity (no/invalid token). 403 = known identity, no permission. "If a valid viewer token hits an admin endpoint and gets 401, that's wrong β€” the token was valid, so it must be 403. Returning 401 for everything is a junior pattern and it leaks which endpoints exist."

400 vs 422: 400 = the request is malformed (bad JSON, missing field). 422 = the request is well-formed but the values fail business rules (e.g., a past date for a future-only field). Some APIs collapse both into 400.

409 vs 422: 409 is a state conflict (duplicate, stale version); 422 is a value validation failure.

Follow-up β€” "When is 200 the wrong code?" "When something was created (should be 201), when DELETE returns no body (204), or when an async op was only accepted (202). Returning 200 for everything loses semantics that monitoring and caching rely on."


3.4 REST vs SOAP vs GraphQL vs gRPC (brief)

REST SOAP GraphQL gRPC
Format JSON XML JSON Protobuf (binary)
Transport HTTP HTTP/SMTP HTTP (one endpoint) HTTP/2
Endpoints Many, resource-based One (/api) One (/graphql) Service methods
Schema Optional (OpenAPI) Strict (WSDL) Strict (SDL) Strict (.proto)
Speed Fast Slow Fast (one round-trip) Fastest
Use case Most modern APIs Banking/legacy Frontends picking fields Service-to-service

GraphQL testing gotchas: single endpoint (every test is a POST to /graphql); it returns 200 even on logical errors, so you check the errors array, not the status; test field-selection variations and schema-introspection drift.

String query = "query { user(id: \"42\") { email name } }";
given().contentType(JSON).body(Map.of("query", query))
.when().post("/graphql")
.then().statusCode(200)
    .body("data.user.email", equalTo("a@x.com"))
    .body("errors", nullValue());

Follow-up β€” "When SOAP over REST?" "Legacy banking/telecom where the strict WSDL contract and built-in WS-* transactional/security guarantees matter."


3.5 The API test plan β€” what to test (9 dimensions)

When asked "how would you test POST /users?", give this structure:

# Dimension What you check
1 Positive / happy path Valid payload β†’ 201, correct body, server-generated ID; follow-up GET confirms persistence
2 Input validation Missing fields β†’ 400; bad email/password format; boundaries, Unicode, emoji
3 Authentication No / expired / tampered token β†’ 401
4 Authorization Token without permission β†’ 403 (not 401); cross-tenant β†’ 403/404
5 Conflict Duplicate email β†’ 409; version mismatch β†’ 412
6 Edge cases Empty optional fields, very long inputs, injection-shaped strings
7 Schema Pipe response through JSON Schema validator to catch contract drift
8 Performance & limits Response time under SLA; oversized body β†’ 413; rapid calls β†’ 429
9 Security Injection sanitized, XSS encoded, HTTPS enforced, no stack-trace leaks

Plus cleanup β€” every test deletes what it created.

Follow-up β€” "How is testing GET different?" "Happy path 200, unknown ID 404, authz 403, pagination (?page=2&size=50 returns the right slice and caps oversized size), filtering/sorting actually take effect, plus schema validation."


3.6 Rest Assured given / when / then

Speakable answer: "Rest Assured gives a BDD-style fluent API β€” given sets up the request, when fires the HTTP call, then asserts. It auto-parses JSON via JsonPath, has built-in Hamcrest matchers, schema validation, and plugs straight into TestNG/JUnit."

import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@Test
public void getUser() {
    given()
        .baseUri("https://api.example.com")
        .header("Authorization", "Bearer " + token)
        .header("Accept", "application/json")
    .when()
        .get("/users/42")
    .then()
        .statusCode(200)
        .body("name", equalTo("Rohan"))
        .body("email", containsString("@"))
        .time(lessThan(2000L));
}
  • given() β€” base URI, headers, body, auth, query/path params, filters
  • when() β€” the verb: .get()/.post()/.put()/.patch()/.delete()
  • then() β€” status, headers, body, time, cookies

Pitfall (real, from my OrdersAPI helper): RestAssured.baseURI = baseUrl; is a static field mutation β€” two parallel tests overwrite each other's base URI. Use instance-scoped given().baseUri(baseUrl) instead.

Follow-up β€” "Why is RestAssured.baseURI = uri dangerous?" "It writes to a global static. Thread A sets URL A, thread B sets URL B, A's next call goes to B. Instance-scoped given().baseUri(...) keeps state per request."


3.7 RequestSpecBuilder / ResponseSpecBuilder (DRY)

Speakable answer: "Specs let me define base URI, common headers, auth, and filters once, then reuse them with .spec(...). Same idea on the response side for common assertions like status 200 and content-type JSON. Change once, applies everywhere."

public class TestSpecs {
    public static RequestSpecification authRequest(String token) {
        return new RequestSpecBuilder()
            .setBaseUri("https://api.example.com")
            .setContentType(ContentType.JSON)
            .addHeader("Authorization", "Bearer " + token)
            .addHeader("Accept", "application/json")
            .addFilter(new RequestLoggingFilter())
            .addFilter(new ResponseLoggingFilter())
            .build();
    }
    public static ResponseSpecification okJson() {
        return new ResponseSpecBuilder()
            .expectStatusCode(200)
            .expectContentType(ContentType.JSON)
            .expectResponseTime(Matchers.lessThan(2000L))
            .build();
    }
}

// usage
given().spec(TestSpecs.authRequest(token)).pathParam("id", 42)
.when().get("/users/{id}")
.then().spec(TestSpecs.okJson()).body("name", equalTo("Rohan"));

Trade-off: specs reduce duplication, but over-baking assertions into a shared response spec can make negative tests awkward β€” keep response specs to genuinely universal expectations.

Follow-up β€” "RequestSpecification vs testng.xml <parameter>?" "Spec is request-shape reuse in code; <parameter> is a single config value per <test> like browser/env."


3.8 POJO bodies + Jackson/Gson (serialize/deserialize)

Speakable answer: "I model request and response as POJOs and let Jackson serialize/deserialize automatically. The win is compile-time field-name safety β€” if the backend renames email to emailAddress, I update one annotation and every test using that POJO fails fast at build, instead of getting a confusing runtime 400. I only hand-write JSON strings for trivial one-offs or when I'm deliberately testing malformed JSON."

public class User {
    private String name;
    private String email;
    private int age;
    // getters, setters, no-arg constructor
}

User u = new User(); u.setName("Rohan"); u.setEmail("r@x.com"); u.setAge(28);

User created = given()
    .contentType(JSON)
    .body(u)                       // POJO -> JSON (Jackson auto-detected on classpath)
.when().post("/users")
.then().statusCode(201)
    .extract().as(User.class);     // JSON -> POJO

Field-level control (Jackson):

@JsonProperty("user_name") private String name;     // rename
@JsonIgnore private String internalId;              // never serialized
@JsonProperty(access = Access.READ_ONLY) private String createdAt;  // read, never sent

Why over raw JSON strings: string concatenation has no compile-time checks, no autocomplete, breaks on quotes in values, and can't be refactored by the IDE. Rest Assured auto-detects Jackson, Gson, or JSON-B on the classpath.

Middle ground (Gson JsonObject): typed wrapper around JSON without full POJO safety β€” works but loses the compile-time field check.

Follow-up β€” "Jackson vs Gson?" "Both work; Rest Assured auto-picks whichever is on the classpath. Jackson is the de-facto standard in Spring shops and has richer annotations; Gson is lighter. Don't mix both in one project."


3.9 Response validation β€” JsonPath/GPath, Hamcrest, schema

Status, headers, time, cookies:

.then()
    .statusCode(200)
    .statusCode(anyOf(is(200), is(201)))
    .header("Content-Type", "application/json")
    .header("X-RateLimit-Remaining", notNullValue())
    .time(lessThan(2000L))                  // milliseconds
    .cookie("session_id", notNullValue());

Body with JsonPath + Hamcrest:

.body("name", equalTo("Rohan"))
.body("age", greaterThan(18))
.body("address.city", equalTo("Bangalore"))   // nested
.body("orders[0].id", equalTo(101))           // index
.body("orders.size()", greaterThan(0))        // length
.body("orders.id", hasItems(101, 102))        // collected
.body("createdAt", matchesRegex("\\d{4}-\\d{2}-\\d{2}T.*"));

Groovy GPath β€” closures for complex queries (powerful, no Java extraction needed):

.body("orders.findAll { it.status == 'paid' }.size()", equalTo(3))
.body("orders.findAll { it.amount > 1000 }.id", hasItems(101, 105))
.body("users.max { it.age }.name", equalTo("Rohan"))
.body("orders.sum { it.amount }", equalTo(15000));

AssertJ SoftAssertions (collect all field mismatches in one run):

SoftAssert sa = new SoftAssert();
sa.assertThat(jp.getString("description[0].orderId")).as("orderId").isEqualTo(orderId);
sa.assertThat(jp.getString("description[0].orderStatus")).as("orderStatus").isEqualTo("created");
sa.assertAll();   // CRITICAL β€” without this, failures pass silently

JSON Schema validation (contract testing inside one repo):

import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

given().get("/users/1")
.then().statusCode(200)
    .body(matchesJsonSchemaInClasspath("schemas/user.json"));
Schema (src/test/resources/schemas/user.json):
{
  "type": "object",
  "required": ["id", "email", "name"],
  "properties": {
    "id":    { "type": "integer", "minimum": 1 },
    "email": { "type": "string", "format": "email" },
    "role":  { "type": "string", "enum": ["admin", "viewer", "editor"] }
  },
  "additionalProperties": false
}
Dependency: io.rest-assured:json-schema-validator. Catches a renamed field, a type change (age int β†’ string), a missing required field, or (with additionalProperties:false) an unexpected new field β€” the moment it happens, not later when an assertion fails confusingly.

Contract testing beyond schema: for cross-team APIs use Pact (consumer records expected shape β†’ provider verifies against it) or validate every response against the OpenAPI spec at runtime.

Follow-up β€” "Why schema validation if you already assert fields?" "Field assertions only cover the fields I named; schema validates the whole structure, types and enums, so drift on an unasserted field still fails."


3.10 extract() + chained calls (login β†’ token β†’ use β†’ cleanup)

Speakable answer: "Helpers should return the Response; tests should assert. I extract the token once, reuse it, and self-clean in the same flow so a partial failure doesn't pollute later runs."

// Extract single path
String token = given().body(login).post("/auth/login")
    .then().statusCode(200).extract().path("data.access_token");

// Extract whole response / typed
Response r = given().get("/users").then().extract().response();
List<User> users = r.jsonPath().getList("data", User.class);
User one = given().get("/users/1").then().extract().as(User.class);

Full lifecycle (the bread and butter):

@Test
public void createReadDeleteUser() {
    String token = given().contentType(JSON)
        .body(Map.of("email","admin@x.com","password","Test@123"))
    .when().post("/auth/login")
    .then().statusCode(200).extract().path("token");

    String email = "rohan+" + System.currentTimeMillis() + "@x.com";  // unique -> parallel-safe
    int id = given().auth().oauth2(token).contentType(JSON)
        .body(Map.of("name","Rohan","email",email))
    .when().post("/users")
    .then().statusCode(201).body("email", equalTo(email)).extract().path("id");

    given().auth().oauth2(token).get("/users/" + id)
        .then().statusCode(200).body("name", equalTo("Rohan"));

    given().auth().oauth2(token).delete("/users/" + id).then().statusCode(204);

    given().auth().oauth2(token).get("/users/" + id)
        .then().statusCode(404);   // verify hard-delete, not soft-delete-as-200
}

Best practices: unique data (timestamp/UUID), extract-once-reuse, self-cleanup in the same test, verify cleanup with a GET-after-DELETE.

Pitfall: don't bake Assert.assertEquals(status, 200) inside a helper β€” it makes the helper unusable for negative tests because it throws before your test can assert.

Follow-up β€” "How do you clean up?" "Best: delete in-flow. Fallback: @AfterMethod using captured IDs. Last resort: a nightly job deleting test data older than 24h."


3.11 Authentication patterns

Pattern Rest Assured
Basic .auth().basic("u","p") / .auth().preemptive().basic(...) (skip 401 challenge)
Bearer / OAuth2 .auth().oauth2(token) or .header("Authorization","Bearer "+token)
API key .header("X-API-Key", key) or .queryParam("api_key", key)
Form .auth().form("u","p", new FormAuthConfig("/login","username","password"))
Cookie/session extract JSESSIONID from login, then .cookie("JSESSIONID", id)
// OAuth2 token from login -> reuse
String token = given().contentType(JSON)
    .body(Map.of("email","u@x.com","password","p"))
.when().post("/auth/login")
.then().statusCode(200).extract().path("access_token");

OTP flow (two-step):

// 1. trigger OTP
given().contentType(JSON).header("x-api-key", apiKey)
    .body(Map.of("email","u@x.com"))
.when().post("/auth/login").then().statusCode(200);

// 2. verify (test env returns a deterministic OTP)
String token = given().contentType(JSON).header("x-api-key", apiKey)
    .body(Map.of("email","u@x.com","otp","111111"))
.when().post("/auth/verify")
.then().statusCode(200).extract().path("access_token");

JWT testing: valid works; expired β†’ 401; tampered payload (original signature) β†’ 401; missing/alg=none β†’ 401; valid token lacking required claim like role β†’ 403. The payload is readable by anyone (base64) β€” the signature is the security.

CSRF (cookie-session apps): GET first to receive the CSRF cookie+token, then send it back in X-CSRF-Token on the POST while sending the session cookie.

Follow-up β€” "Preemptive basic vs basic?" "Plain basic waits for a 401 challenge then resends with the header; preemptive sends the Authorization header on the first request β€” faster and needed for servers that don't issue a challenge."


3.12 Filters (logging, auto-auth, audit, report attach)

Speakable answer: "Filters are cross-cutting hooks around every request. I use them for logging, auto-injecting the auth header, audit, and attaching request/response to the report β€” so test code stays clean."

// Built-in logging
RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());
given()... .then().log().ifValidationFails();   // log only on failure -> less CI noise

// Auto-inject auth on every call
public class AuthFilter implements Filter {
    private final String token;
    public AuthFilter(String token) { this.token = token; }
    public Response filter(FilterableRequestSpecification req,
                           FilterableResponseSpecification res, FilterContext ctx) {
        req.header("Authorization", "Bearer " + token);
        return ctx.next(req, res);
    }
}
RestAssured.filters(new AuthFilter(token));

// Audit every call (lambda filter)
RestAssured.filters((req, res, ctx) -> {
    long start = System.currentTimeMillis();
    Response r = ctx.next(req, res);
    AuditLog.log(req.getMethod(), req.getURI(), r.statusCode(),
                 System.currentTimeMillis() - start);
    return r;
});

Allure/Extent attach: a filter calling Allure.addAttachment("Request"/"Response", ...) puts the full payloads into the report automatically.

Follow-up β€” "Why a filter instead of adding the header in each test?" "Single source of truth β€” if auth changes, I change one filter, not every test. Same reason logging and audit belong in filters, not test bodies."


3.13 Postman vs Rest Assured (brief)

Postman Rest Assured
Form GUI + JS scripts (Newman for CI) Java code
Strength Fast exploration, sharing collections, mock servers Versioned-in-repo, POJO type-safety, schema validation, runs in the test pyramid with TestNG
Best for Manual/exploratory, quick contract checks, non-engineers Automated regression owned by SDETs, parallel runs, CI gating

Speakable answer: "I use Postman to explore an API and build a quick collection, but the durable regression suite lives in Rest Assured β€” it's versioned with the code, gives compile-time POJO safety, and runs as part of the build with the rest of the pyramid."


3.14 Common pitfalls (the seniority signals)

Pitfall Why bad Fix
RestAssured.baseURI = uri Static mutation breaks parallel runs Instance-scoped given().baseUri(uri)
Assertions inside API helpers Helper unusable for negative tests Helper returns Response; test asserts
JSON string bodies No compile-time field safety POJOs + Jackson
Two JSON libs mixed (Gson + org.json) Conversion overhead, confusion Standardize on one (Jackson)
Hardcoded secrets/API keys Secret in source control Env var / vault
No schema validation Backend rename silently corrupts test matchesJsonSchemaInClasspath
System.out.println everywhere Noisy, no structure Logging filters + log().ifValidationFails()
Forgetting softAssert.assertAll() Failures pass silently Always call assertAll()
No retry on transient 5xx Network flake fails the run Filter that retries 502/503/504 with backoff

The line that signals seniority: "Helpers should return responses; tests should assert. And RestAssured.baseURI = uri is a static mutation that breaks parallel runs β€” always instance-scoped."


3.15 Scenario Q&A

Q1. Test a POST /users endpoint end to end.

"The 9 dimensions: positive (201 + body + ID + follow-up GET), input validation (missing/bad fields β†’ 400), authentication (no/expired/tampered token β†’ 401), authorization (wrong-permission token β†’ 403, not 401), conflict (duplicate email β†’ 409), edge cases, schema validation, performance/limits (413, 429, SLA), security (injection/XSS/no stack traces), then cleanup."

Q2. You have dependent APIs β€” create-order needs a logged-in token and a product ID. How do you structure it?

"Chain with extract: POST /auth/login β†’ .extract().path('token'); create the product or read a known one β†’ extract its ID; create the order with both. Each step asserts before extracting so a failure is localized. Token extracted once and reused. Clean up in reverse order at the end."

Q3. The response is large and deeply nested β€” how do you assert just the parts you care about?

"Groovy GPath closures: body(\"items.findAll { it.status=='paid' }.size()\", equalTo(3)) or body(\"data.order.items[0].sku\", equalTo('SKU-1')). For structure-wide correctness I add matchesJsonSchemaInClasspath so I'm not asserting every field by hand but still catch drift."

Q4. How do you handle token expiry in a long-running suite?

"Fetch the token in a @BeforeClass/@BeforeSuite and cache it. Wrap auth in a filter that, on a 401, re-authenticates once and retries. For deliberately testing expiry, I either wait out a short-lived test token or use a pre-minted expired token and assert 401."

Q5. Walk me through negative testing.

"I deliberately break each dimension and assert the right error code: malformed JSON β†’ 400, missing field β†’ 400/422, wrong-type field β†’ 400/422, no token β†’ 401, wrong-permission token β†’ 403, duplicate β†’ 409, oversized body β†’ 413, rate limit β†’ 429. Crucially, my helpers return the Response so I can reuse them for these β€” they don't assert 200 internally."

Q6. How do you keep tests independent for parallel runs?

"Unique data per test β€” UUID or timestamp-suffixed emails β€” so two threads don't collide. No shared static state (the RestAssured.baseURI trap). Instance-scoped specs. Each test creates and deletes its own data. TestNG parallel='methods' with a sane thread-count."

Q7. The backend renamed a field from dealId to id. How would your suite catch it immediately rather than confusingly later?

"Schema validation flags it on the first response. With POJOs, I update the @JsonProperty once and every dependent test compile-fails or fails fast. Without either, the value just comes back null and an assertion three steps later fails with no obvious cause."

Q8. A POST occasionally returns 503 due to a flaky upstream. Tests fail intermittently. What do you do?

"Add a filter that retries 502/503/504 a couple of times with backoff β€” that's a transient infra condition, not a product bug. But I keep a separate, explicit test asserting graceful behavior under sustained 503 so I don't mask a real outage."

Q9. How do you test file upload?

"given().multiPart('file', new File('invoice.pdf')).multiPart('metadata', json, 'application/json').post('/upload'). Then GET the returned file URL to confirm storage. Negatives: oversized β†’ 413, wrong content-type β†’ 415, path-traversal name doesn't escape the upload dir, zero-byte file."

Q10. Two sellers update the same order concurrently and both get 200 β€” one silently overwrites the other. How would you catch and fix this?

"A parallel test hitting the same resource from two threads exposes it β€” the loser's response body differs from what it sent. The fix is optimistic concurrency: an If-Match etag, so the stale writer gets 412 Precondition Failed. The test then explicitly asserts the loser gets 412."

Q11. How do you do data-driven API testing in Java?

"TestNG @DataProvider returning Object[][] β€” each row including the expected status β€” drives one @Test across positive and negative cases. For larger data I read from a JSON file (data is data, non-engineers can edit it) or CSV via Apache Commons CSV; the trade-off is losing compile-time field safety."

Q12. How do you test rate limiting without breaking the rest of the suite?

"Run the rate-limit assertion as an isolated test β€” fire N rapid calls and assert the (N+1)th returns 429. For the normal suite, I pace requests (a throttle/backoff) so I don't trip the limit during unrelated tests."

4. CORE CS (OOP, Collections, Exceptions) + SQL

Infosys lateral SDET rounds lean hard on OOP fundamentals, the Collections framework, and SQL. For every concept below: a speakable answer, a snippet, the why/trade-off, and a likely follow-up. Python equivalents are noted in one line where they help.


4.1 OOP β€” the 4 pillars (with a real example each)

Pillar One-line answer Real example
Encapsulation Bundle data + behavior, hide internal state behind methods private balance exposed only via deposit()/withdraw()
Abstraction Expose what an object does, hide how List interface β€” you call add(), don't care if it's ArrayList or LinkedList
Inheritance A subclass reuses/extends a parent's behavior (IS-A) class SavingsAccount extends Account
Polymorphism One interface, many implementations; resolved at runtime Payment p = new UpiPayment(); p.pay()
// Encapsulation + Inheritance + Polymorphism in one
abstract class Account {                 // abstraction
    private double balance;              // encapsulation
    public void deposit(double a) { if (a > 0) balance += a; }
    public double getBalance() { return balance; }
    public abstract double interest();  // each subtype defines its own
}
class Savings extends Account {          // inheritance
    public double interest() { return getBalance() * 0.04; }
}
Account acc = new Savings();             // polymorphism
acc.interest();                          // runtime dispatch -> Savings.interest()
  • Why it matters: encapsulation localizes change (you can swap the storage of balance without touching callers); polymorphism lets you write code against a type and extend behavior without editing existing classes (Open/Closed).
  • Follow-up β€” "compile-time vs runtime polymorphism?" Overloading = compile-time (static binding by signature); overriding = runtime (dynamic dispatch by actual object type).
  • Python note: same four pillars; encapsulation is by convention (_field), polymorphism is duck typing (no shared base needed).

4.2 Abstraction vs Encapsulation (commonly confused)

  • Abstraction = design-level: hiding complexity, showing only the essential contract (interfaces, abstract classes).
  • Encapsulation = implementation-level: hiding data, controlling access (private fields + getters/setters).
  • Speakable line: "Abstraction is about what to expose; encapsulation is about how to protect it. A List interface is abstraction; private array inside ArrayList is encapsulation."
  • Follow-up β€” "Can you have one without the other?" Yes β€” you can encapsulate a concrete class with no abstraction, or abstract over data that isn't well-encapsulated. Good design uses both.

4.3 Interface vs Abstract class

Interface Abstract class
Multiple inheritance Yes (implement many) No (extend one)
State (fields) Only public static final constants Can have instance fields
Methods abstract; default/static (Java 8+); private (Java 9+) abstract + concrete
Constructor No Yes
Use when Defining a capability/contract (Comparable, Runnable) Sharing common state + partial implementation among related types
interface Drivable { int MAX = 200; void drive(); default void honk(){ System.out.println("beep"); } }
abstract class Vehicle { protected String reg; abstract void start(); void stop(){ /*shared*/ } }
  • Why: prefer interfaces for flexibility (a class can implement several); use an abstract class only when subtypes genuinely share code/state.
  • Follow-up β€” "Why default methods in Java 8?" To evolve interfaces (e.g., add stream() to Collection) without breaking every existing implementer.
  • Python note: abc.ABC + @abstractmethod for abstract classes; no pure interface keyword β€” ABCs or typing.Protocol fill that role.

4.4 Overloading vs Overriding

class Printer {
    void print(int x) {}             // overload: same name,
    void print(String x) {}          // different parameter list (compile-time)
}
class Color extends Printer {
    @Override void print(int x) {}   // override: same signature, subclass (runtime)
}
Overloading Overriding
Where Same class Subclass
Signature Must differ (params) Must match exactly
Return type Can differ Same or covariant
Binding Compile-time (static) Runtime (dynamic)
Access modifier Any Cannot reduce visibility
  • Follow-up β€” "Can you override a static method?" No β€” statics are hidden, not overridden (resolved by reference type, not object). Same for private/final methods.
  • Follow-up β€” "Can return type alone overload?" No β€” Java can't disambiguate by return type alone.

4.5 equals() & hashCode() contract

The 3 rules: (1) a.equals(b) true β‡’ a.hashCode()==b.hashCode() MUST be true. (2) equals false β‡’ hashCodes MAY still collide. (3) hashCode is stable across calls if state unchanged.

@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User)) return false;
    return Objects.equals(email, ((User) o).email);   // identity = email
}
@Override public int hashCode() { return Objects.hash(email); }  // SAME field
  • Why: hash-based collections (HashMap, HashSet) locate the bucket via hashCode(), then confirm with equals(). Override one without the other and set.contains(equalObject) returns false β€” silent data loss.
  • equals properties: reflexive, symmetric, transitive, consistent, and x.equals(null)==false.
  • Follow-up β€” "instanceof vs getClass()?" instanceof lets subclasses be equal (can break symmetry); getClass() is stricter. Pick based on your inheritance design.
  • Python note: override __eq__ and __hash__ together β€” same contract.

4.6 String immutability + String pool, == vs .equals()

Strings are immutable. Reasons: (1) string pool sharing of literals, (2) security (paths/URLs can't be tampered after validation), (3) cached hashCode, (4) thread safety.

String a = "hello";              // pooled
String b = "hello";              // reuses same pooled object
String c = new String("hello");  // forces a new heap object
a == b;        // true  (same reference)
a == c;        // false (different objects)
a.equals(c);   // true  (content)
c.intern()==a; // true  (intern returns pool ref)
  • == vs .equals(): == compares references (identity); .equals() compares content. Always use .equals() for value comparison.
  • Follow-up β€” "How many objects: new String("hi")?" Two β€” "hi" in the pool + one on the heap.
  • Follow-up β€” "StringBuilder vs StringBuffer?" Both mutable; StringBuffer is synchronized (slower), StringBuilder is not. Use StringBuilder for loop concatenation to avoid O(nΒ²) garbage.
  • Python note: == is __eq__, is is identity; small ints/interned strings are cached (gotcha mirrors == vs == in Java).

4.7 Java Collections framework

Collection
β”œβ”€β”€ List   (ordered, duplicates)        -> ArrayList, LinkedList, Vector
β”œβ”€β”€ Set    (no duplicates)              -> HashSet, LinkedHashSet, TreeSet
└── Queue/Deque                         -> ArrayDeque, LinkedList, PriorityQueue
Map (key->value, NOT a Collection)      -> HashMap, LinkedHashMap, TreeMap, Hashtable
Type Ordering Nulls Notes
ArrayList insertion yes random access O(1)
LinkedList insertion yes good as Deque only
HashSet none one null backed by HashMap
LinkedHashSet insertion one null predictable iteration
TreeSet sorted no null red-black tree, O(log n)
HashMap none 1 null key O(1) avg
LinkedHashMap insertion/access 1 null key LRU cache base
TreeMap sorted by key no null key O(log n), navigable

ArrayList vs LinkedList

Operation ArrayList LinkedList
get(i) O(1) O(n)
add at end O(1) amortized O(1)
add(0,..) / remove(0) O(n) shift O(1)
memory / cache locality low / excellent high / poor
  • The real answer: ArrayList wins almost always β€” CPU cache loves contiguous memory, so even O(n) shifts beat LinkedList's pointer-chasing. Use ArrayDeque over LinkedList for queues.
  • Follow-up β€” "Why is ArrayList.add() O(1) amortized?" Most adds are O(1); when full it grows ~50% (O(n) rare), averaged out to O(1).

HashMap internals (Java 8+)

  1. put(k,v) computes hash(k) (spreads high bits via XOR). 2. Index = hash & (n-1) (n is a power of 2 β†’ cheap bitmask instead of modulo). 3. Empty bucket β†’ store node. 4. Collision β†’ compare with equals(); update or append to the bucket's linked list. 5. Treeify: when a single bucket reaches 8 nodes and table β‰₯ 64, the list becomes a red-black tree β†’ O(log n) instead of O(n). 6. Load factor 0.75 β†’ resize (double + rehash) at 12 entries for default 16.

  2. Complexity: O(1) average get/put; O(log n) worst with treeification; O(n) only with a pathological hashCode().

  3. Follow-up β€” "HashMap vs Hashtable vs ConcurrentHashMap?" Hashtable = legacy, fully synchronized (slow). ConcurrentHashMap = bucket/bin-level locking + CAS β†’ thread-safe and fast under contention.

Fail-fast vs fail-safe iterators

  • Fail-fast (ArrayList, HashMap): track a modCount; structural modification during iteration throws ConcurrentModificationException. Use the iterator's remove() or a removeIf() instead.
  • Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap): iterate over a snapshot/segment, no exception, may not see latest writes.
  • Python note: mutating a dict/list mid-iteration raises RuntimeError/skips β€” similar fail-fast spirit.

4.8 Exceptions

Throwable
β”œβ”€β”€ Error            (JVM, unrecoverable β€” OutOfMemoryError)  -> don't catch
└── Exception
    β”œβ”€β”€ RuntimeException (unchecked β€” NPE, IllegalArgument)
    └── checked (IOException, SQLException)                   -> must handle/declare
  • Checked = compiler forces throws or try/catch; use for recoverable conditions (file missing). Unchecked = programming bugs (null, bad arg).
// try-with-resources auto-closes (AutoCloseable), even on exception
try (BufferedReader r = new BufferedReader(new FileReader("a.txt"))) {
    return r.readLine();
} catch (IOException e) {
    throw new DataLoadException("read failed", e);   // custom + chaining
} finally {
    // always runs (except System.exit / JVM crash)
}

class DataLoadException extends RuntimeException {     // custom unchecked
    DataLoadException(String m, Throwable cause) { super(m, cause); }
}
  • try/finally gotcha: a return in finally swallows exceptions and overrides earlier returns β€” avoid it.
  • Follow-up β€” "Why do many devs dislike checked exceptions?" Verbose throws chains; they don't compose with lambdas. Modern frameworks (Spring) lean unchecked.
  • Python note: all exceptions are unchecked; try/except/else/finally; custom via class MyError(Exception).

4.9 Stream API (filter / map / collect)

List<String> out = users.stream()
    .filter(u -> u.getAge() >= 18)        // intermediate, lazy
    .map(User::getName)                   // transform
    .sorted()
    .collect(Collectors.toList());        // terminal -> triggers

Map<String,List<User>> byCity =
    users.stream().collect(Collectors.groupingBy(User::getCity));

int totalAge = users.stream().filter(u->u.getAge()>=18)
    .mapToInt(User::getAge).sum();
  • Lazy: intermediate ops do nothing until a terminal op runs; the pipeline fuses into a single pass. Streams are single-use.
  • Follow-up β€” "Stream vs Collection?" Collection stores data; Stream processes it (lazy, one-shot). Don't replace simple loops β€” streams add overhead.
  • Follow-up β€” "parallelStream caveat?" Uses the common ForkJoinPool; only worth it for large, CPU-bound, side-effect-free work.
  • Python note: comprehensions / generator expressions + map/filter; itertools for lazy pipelines.

4.10 Comparable vs Comparator

class User implements Comparable<User> {            // natural order
    public int compareTo(User o){ return name.compareTo(o.name); }
}
Comparator<User> byAge = Comparator.comparing(User::getAge)
        .thenComparing(User::getName).reversed();   // external order
users.sort(byAge);
Comparable Comparator
Where compareTo() in the class compare() external
Orderings one "natural" many custom
Modify class? yes no
  • Contract: negative if a<b, 0 if equal, positive if a>b.
  • Follow-up β€” "Should compareTo be consistent with equals?" Strongly recommended; inconsistency corrupts TreeMap/TreeSet behavior.
  • Python note: __lt__ etc. + functools.total_ordering; sorted(key=...) β‰ˆ Comparator.

4.11 final / static / this / super

final int MAX = 100;            // can't reassign (final class -> no subclass; final method -> no override)
static int count;               // belongs to the class, shared across instances
this.name = name;               // current instance / call another constructor: this(...)
super.method();                 // parent's version / super(...) calls parent constructor
  • static block runs once at class load; static methods can't use this/instance fields.
  • Follow-up β€” "final on a reference?" The reference can't be reassigned, but the object's contents can still change (final List can still .add()).

4.12 Singleton pattern

// Lazy + thread-safe: double-checked locking
class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    static Singleton get() {
        if (instance == null)
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        return instance;
    }
}
// Best (Bloch): enum is serialization- & reflection-safe by construction
enum Config { INSTANCE; }
  • Why volatile: without it, partially-constructed object can be visible to another thread.
  • Follow-up β€” "Why enum singleton?" JVM guarantees one instance; immune to reflection and serialization attacks that break the class form.

SQL

4.13 JOINs (with a Venn note)

Join Returns Venn
INNER only matched rows intersection
LEFT all left + matched right (NULLs for unmatched) full left circle
RIGHT all right + matched left full right circle
FULL OUTER all rows from both both circles
CROSS every pair (AΓ—B) cartesian product
SELF table joined to itself e.g. employee↔manager
SELECT e.name, d.name FROM employees e INNER JOIN departments d ON e.dept_id = d.id;
SELECT c.name, o.amount FROM customers c LEFT JOIN orders o ON c.id = o.customer_id;
SELECT e.name AS emp, m.name AS mgr FROM employees e LEFT JOIN employees m ON e.manager_id = m.id; -- self
  • The LEFT JOIN trap: filtering the right table in WHERE (WHERE o.amount > 100) silently turns it into an INNER JOIN by dropping NULL rows. Put right-table conditions in ON.
  • MySQL note: no FULL OUTER JOIN β€” emulate with LEFT JOIN ... UNION ... RIGHT JOIN.
  • Follow-up β€” "Find customers who never ordered?" LEFT JOIN orders ... WHERE o.id IS NULL.

4.14 WHERE vs HAVING

WHERE HAVING
Filters rows groups
Runs before GROUP BY after GROUP BY
Aggregates? no yes
SELECT dept_id, COUNT(*) AS high_earners
FROM employees
WHERE salary > 100000        -- 1. filter rows
GROUP BY dept_id             -- 2. bucket
HAVING COUNT(*) > 3;         -- 3. filter buckets
  • Execution order reminder: FROM β†’ WHERE β†’ GROUP BY β†’ HAVING β†’ SELECT β†’ ORDER BY β†’ LIMIT. (That's why a SELECT alias works in ORDER BY but not WHERE.)

4.15 GROUP BY + aggregates

SELECT dept_id, COUNT(*) cnt, AVG(salary) avg_sal, MAX(salary) top
FROM employees GROUP BY dept_id;
  • Rule: every non-aggregated SELECT column must be in GROUP BY.
  • COUNT(*) counts all rows incl. NULLs; COUNT(col) skips NULLs; COUNT(DISTINCT col) unique non-NULLs.
  • Use COALESCE(SUM(x),0) after a LEFT JOIN so no-match groups show 0 instead of NULL.

4.16 2nd & Nth highest salary

-- Subquery (2nd highest)
SELECT MAX(salary) FROM employees WHERE salary < (SELECT MAX(salary) FROM employees);

-- DENSE_RANK (handles ties, generalizes to Nth)
SELECT salary FROM (
  SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) rnk FROM employees
) t WHERE rnk = 2;            -- change to = N for Nth

-- MySQL LIMIT
SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1;
  • Why DENSE_RANK over RANK? RANK skips numbers after ties (1,1,3); DENSE_RANK doesn't (1,1,2) β€” correct for "Nth distinct salary."

4.17 Find & delete duplicates

-- Find duplicate emails
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;

-- Delete dupes, keep lowest id (MySQL self-join)
DELETE u1 FROM users u1 JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id;

-- Portable (window function)
DELETE FROM users WHERE id IN (
  SELECT id FROM (
    SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) rn FROM users
  ) t WHERE rn > 1
);

4.18 Window functions

Like GROUP BY, but returns one row per input row (no collapsing).

SELECT name, salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) rn,   -- 1,2,3,4
  RANK()       OVER (ORDER BY salary DESC) rk,   -- 1,1,1,4 (skips)
  DENSE_RANK() OVER (ORDER BY salary DESC) dr    -- 1,1,1,2 (no skip)
FROM employees;
Function Behavior on ties
ROW_NUMBER always unique
RANK same rank, skips next
DENSE_RANK same rank, no skip
-- Top 3 per dept (PARTITION resets ranking per group)
SELECT * FROM (
  SELECT name, dept_id, salary,
    DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) rnk
  FROM employees
) t WHERE rnk <= 3;

-- LEAD/LAG: peek next/previous row (e.g. month-over-month growth)
SELECT month, sales, sales - LAG(sales) OVER (ORDER BY month) AS growth FROM monthly_sales;

-- Running total
SELECT id, amount, SUM(amount) OVER (ORDER BY id) AS running_total FROM orders;
  • OVER anatomy: PARTITION BY (group), ORDER BY (within partition), optional frame ROWS BETWEEN ....

4.19 Subqueries & correlated subqueries

-- Non-correlated: inner runs once
SELECT * FROM orders WHERE amount > (SELECT AVG(amount) FROM orders);

-- Correlated: inner references outer, runs per outer row
SELECT e.name, (SELECT COUNT(*) FROM employees e2 WHERE e2.salary > e.salary) AS above
FROM employees e;
  • EXISTS vs IN: EXISTS short-circuits on first match (better for large/correlated subqueries) and is NULL-safe; IN is fine for small static lists. NOT IN with a NULL in the subquery returns no rows β€” a classic bug; prefer NOT EXISTS.
  • Follow-up β€” "Correlated subquery perf?" Runs N times; often rewritable as a JOIN or window function.

4.20 Normalization (1NF–3NF)

  • 1NF: atomic values, no repeating groups (no comma-separated phones).
  • 2NF: 1NF + no partial dependency on part of a composite key (move product_name out of order_items).
  • 3NF: 2NF + no transitive dependency (dept_name depends on dept_id, so it belongs in departments, not employees).
  • Denormalize for read-heavy reporting/warehouses where joining many tables is too slow.

4.21 Indexes (when / why)

  • An index is a separate B-tree mapping column values β†’ row locations: faster reads, slower writes + extra space.
  • Help: WHERE / JOIN / ORDER BY / on selective (high-cardinality) columns.
  • Don't help: function on column (WHERE LOWER(email)=...), leading wildcard (LIKE '%han'), tiny tables, low-cardinality columns (gender).
  • Composite (A,B,C) leftmost-prefix rule: usable for filters on A, A+B, A+B+C β€” not B, C, or B+C alone.
  • Follow-up β€” "How to verify index use?" EXPLAIN; watch for type=ALL (full scan) and Using filesort.

4.22 DELETE vs TRUNCATE vs DROP

DELETE TRUNCATE DROP
Type DML DDL DDL
Removes rows (WHERE allowed) all rows table + structure
Rollback yes (in txn) no (auto-commit) no
Speed slow (row-by-row, triggers) fast (deallocates pages) fastest
Resets AUTO_INCREMENT no yes n/a

4.23 UNION vs UNION ALL

SELECT name FROM employees UNION     SELECT name FROM contractors;  -- dedups (sort cost)
SELECT name FROM employees UNION ALL SELECT name FROM contractors;  -- keeps dupes (faster)
  • Same column count + compatible types; result uses first SELECT's names. Default to UNION ALL unless you specifically need dedup.

4.24 Practice queries

Q1. Employees earning more than their manager

SELECT e.name FROM employees e JOIN employees m ON e.manager_id = m.id WHERE e.salary > m.salary;
Q2. Department with highest average salary
SELECT d.name, AVG(e.salary) a FROM employees e JOIN departments d ON e.dept_id=d.id
GROUP BY d.name ORDER BY a DESC LIMIT 1;
Q3. Most recent order per customer
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) rn FROM orders
) t WHERE rn = 1;
Q4. Departments with no employees
SELECT d.* FROM departments d LEFT JOIN employees e ON e.dept_id=d.id WHERE e.id IS NULL;
Q5. Count employees per dept, include zero-employee depts
SELECT d.name, COUNT(e.id) cnt FROM departments d
LEFT JOIN employees e ON d.id=e.dept_id GROUP BY d.id, d.name;
Q6. Median salary
SELECT AVG(salary) median FROM (
  SELECT salary, ROW_NUMBER() OVER (ORDER BY salary) rn, COUNT(*) OVER () total
  FROM employees
) t WHERE rn IN (FLOOR((total+1)/2), CEIL((total+1)/2));
Q7. Pivot β€” order counts per status by month
SELECT MONTH(created_at) m,
  SUM(status='COMPLETED') completed,
  SUM(status='PENDING')   pending
FROM orders GROUP BY MONTH(created_at);   -- MySQL: boolean -> 1/0
Q8. Products never ordered
SELECT * FROM products p WHERE NOT EXISTS (
  SELECT 1 FROM order_items oi WHERE oi.product_id = p.id);

4.25 NoSQL / MongoDB (brief)

  • When to use: flexible/evolving schema, nested documents, horizontal scale, high write throughput, no rigid relational integrity. Avoid when you need multi-table ACID transactions and complex joins β€” relational fits better.
  • Model: documents (BSON) in collections; _id is an auto ObjectId. Embed related data you read together; reference data you query independently.
// CRUD
db.users.insertOne({ name: "Rohan", roles: ["sdet"], age: 28 });
db.users.find({ age: { $gte: 25 }, roles: "sdet" });        // $gte, array match
db.users.updateOne({ name: "Rohan" }, { $set: { age: 29 } });
db.users.deleteOne({ name: "Rohan" });

// Aggregation pipeline (GROUP BY equivalent)
db.orders.aggregate([
  { $match: { status: "COMPLETED" } },
  { $group: { _id: "$customerId", total: { $sum: "$amount" } } },
  { $sort:  { total: -1 } }
]);
  • SQL β†’ Mongo map: WHEREβ†’$match, GROUP BYβ†’$group, JOINβ†’$lookup, ORDER BYβ†’$sort, SELECT colsβ†’$project.
  • Follow-up β€” "Does MongoDB support transactions?" Yes since 4.0 (multi-document ACID), but they're heavier than relational β€” schema design usually avoids needing them.

5. FRAMEWORK, PROJECTS, PROCESS & BEHAVIORAL

For Rohan (~5-6 yrs, SDET/QA, currently Questt, earlier Avysh). Speakable answers, the "why," likely follow-ups. Per project: Pitch (30s) β†’ Architecture β†’ 4-6 Q&A β†’ STAR β†’ honest caveats.


5.1 HOW TO EXPLAIN "YOUR FRAMEWORK" (the universal answer)

When an interviewer says "walk me through your framework," they want a mental model, not a folder tour. Use this 6-beat structure regardless of language:

"Every framework I build has the same six layers β€” layers, tools, data, reporting, CI, and the run lifecycle. Let me walk them top to bottom and then trace one test through them."

Layer What lives here Java example (Avysh/Questt) Python/TS example
Test layer @Test methods, assertions only src/test/java/.../module/* TestNG classes pytest test_*.py, Playwright *.spec.ts
Business/helper (facade) Reusable business flows that orchestrate pages/services LoginHelper, OrdersAPIHelper, NavigatorHelper adapters, AuthApi, page flows
Page/Service objects Locators + actions (UI), endpoint wrappers (API) 40+ POM classes; OrdersAPI, GET/POST wrappers Playwright POM pages/, BaseApi
Core/util base Waits, driver, JSON readers, common assertions CommonUtils, WebDriverUtils, ReadTestData DriverUtils, fixtures, conftest.py
Test data Externalized inputs + expected JSON under testdata/, config.properties golden_dataset.json, .env
Reporting/CI HTML reports + pipeline ExtentReports/TestNG + Jenkins/mvn HTML report + GitHub Actions/Jenkins

"How a single test runs" (memorize this trace):

"TestNG/pytest picks up the suite XML or the marker, a @BeforeClass/fixture/global-setup authenticates once and builds the driver or token, the test method calls a helper, the helper drives page objects or service wrappers which sit on the core util base, the test asserts, and a listener or reporter captures pass/fail plus a screenshot or trace. CI runs the whole thing on push, publishes the HTML report, and archives artifacts."

Design patterns in automation (name them, give the why)

Pattern Where I use it The "why" in one line
Page Object Model UI page classes Locators centralized β€” a UI change touches one class, not every test
Factory WebDriverUtils (driver per browser); a DriverFactory is the cleaner shape Caller asks for "Chrome," factory hides construction
Singleton Config/property loader (DriverUtils, config.properties) One config instance shared across the run
Builder / fluent REST Assured given().when().then(); Playwright request builder Readable request construction without telescoping constructors
Strategy Browser selection, weight profiles in eval framework Swap algorithm/behavior at runtime via parameter
Facade Helper layer (LoginHelper) Hide multi-page orchestration behind one call
DI / fixtures pytest fixtures, Playwright global-setup storage state, TestNG params Inject auth/driver/data instead of hard-wiring
Adapter API vs UI adapters returning one normalized payload (eval framework) Evaluators don't care which channel produced the answer

POM, data-driven, BDD β€” the three "framework type" concepts

  • POM: each page = a class of locators + actions. Tests read as business steps; locator maintenance is isolated.
  • Data-driven: inputs externalized (JSON/Excel/CSV/@DataProvider). Same test logic, many data rows. Questt API framework is JSON-driven via ReadTestData; Avysh used Gson JSON (ordered LinkedHashMap for UI, JsonObject for API).
  • BDD (concept β€” be honest I haven't shipped Cucumber): Given/When/Then in Gherkin (Cucumber/SpecFlow/Behave), step defs map phrases to code. Value is business-readable specs and living documentation; cost is the glue layer and step-def maintenance. "I understand BDD and could adopt it; my current frameworks favor a helper-facade layer that gives the same readability without the Gherkin overhead."

TestNG vs pytest structure (common compare question)

TestNG (Java) pytest (Python)
Test marker @Test, groups def test_*, @pytest.mark
Setup/teardown @BeforeClass/@BeforeMethod fixtures (conftest.py, scopes)
Data-driven @DataProvider @pytest.mark.parametrize
Suite config testng.xml (parallel, listeners, params) pytest.ini/pyproject.toml, CLI markers
Parallel parallel="classes" thread-count=N pytest-xdist -n
Listeners IReporter, IRetryAnalyzer, IAnnotationTransformer hooks/plugins, fixtures

Git branching & CI/CD

  • Branching: trunk-based feature branches off main/develop, PR + review + green CI before merge. Conventions: feature/, bugfix/, hotfix/. Hotfix branches off main for prod emergencies.
  • Typical flow I follow: git checkout -b feature/X β†’ commit small β†’ push β†’ open PR β†’ CI runs smoke + lint β†’ review β†’ squash-merge β†’ CI on main runs regression. Rebase to keep history clean; never force-push a shared branch.
  • CI/CD pipeline stages: Checkout β†’ Build β†’ Unit Test β†’ Static Analysis β†’ Package β†’ Deploy(staging) β†’ Smoke β†’ Integration β†’ Promote.
  • CI vs CD vs CD: Continuous Integration = merge often + auto build/test. Continuous Delivery = auto-deploy to staging on green. Continuous Deployment = auto-deploy to prod on green.
  • Where QA fits: smoke E2E on PR; API tests after deploy-to-staging; full regression nightly. Reports published as pipeline artifacts so devs self-serve.
  • My tooling: Jenkins (Morrie Jenkinsfile, parameterized TEST_SUITE = all/api/ui/e2e), GitHub Actions (HOAD eval eval_ci.yml β€” runs on every PR, uploads HTML report as artifact), mvn clean test -Db2b.testng.xml=<suite> for the Java suites.

The 60-second "framework" answer (say this verbatim under pressure)

"I think of any framework as six layers. At the top, thin test methods that only assert. Below them, a helper or facade layer that holds reusable business flows so tests read like English. Below that, page objects or service wrappers that own locators or endpoints. Underneath, a core utility base β€” waits, driver factory, data readers. Test data is externalized as JSON or fixtures so logic and data are separate. And cross-cutting are reporting and CI. When a test runs: the runner reads the suite, a setup hook authenticates once and builds the driver or token, the test calls a helper, the helper drives page or service objects on the util base, the test asserts, a listener captures the result with a screenshot or trace, and CI publishes the report. The win of layering this way is that a UI change touches one page class, a flow change touches one helper, and tests stay readable."


5.2 PROJECT DEEP-DIVE (A): Questt Rest Assured API Framework

Path: /Users/rohan/eclipse-workspace/RestAssured_API β€” built from scratch for Questt (ed-tech app). Rest Assured 5.1.1 + TestNG + Maven, Java.

Pitch (30s)

"At Questt I built a Rest Assured API automation framework from scratch for our ed-tech platform. It's Rest Assured 5.1.1 with TestNG and Maven β€” about 62 test methods across 15 classes, 7 active suites wired in a questt_API.xml. It covers the whole API surface: auth and accounts β€” signup, verify-OTP, create-account, switch-account, logout β€” student and teacher profile CRUD, the Navigator module which is dashboard, nodes, quests and submissions, Study Plan v2, Challenges, and the dashboard/content and public no-login endpoints. The design is layered β€” tests call service helpers like LoginHelper, NavigatorHelper, StudyPlanHelper, those helpers use thin HTTP wrappers over Rest Assured, test data is JSON-driven, and responses are validated against 19-plus JSON schemas. It runs with mvn clean test."

Architecture / design

RestAssured_API/
β”œβ”€β”€ pom.xml                         Rest Assured 5.1.1 + TestNG + Maven; POI + OpenCSV
β”œβ”€β”€ questt_API.xml                  TestNG suite β€” 7 active sub-suites
β”œβ”€β”€ helpers / services              LoginHelper, AccessTokenHelper (token + profile switch),
β”‚                                   CreateProfileHelper, NavigatorHelper, StudyPlanHelper,
β”‚                                   ChallengesHelper, SwitchAccountHelper, ResponseDataHelper
β”œβ”€β”€ http wrappers                   GET (3 overloads: with token / without / with API key),
β”‚                                   POST, PATCH
β”œβ”€β”€ util                            ReadTestData (JSON reader), DriverUtils (property loader),
β”‚                                   CommonUtils
β”œβ”€β”€ testdata (JSON)                 request bodies + expected values
β”œβ”€β”€ schemas (19+)                   JSON schema files for response validation
└── config.properties              stage.questt.com base URIs (API v1–v4)
- Layered like the Java UI framework, minus the browser. Tests β†’ service helpers β†’ HTTP wrappers β†’ Rest Assured. Tests stay declarative; auth/token logic lives in AccessTokenHelper. - Token + profile-switch as a first-class concern. AccessTokenHelper handles bearer tokens and profile switching (student↔teacher) β€” Questt accounts can hold multiple profiles, so SwitchAccountHelper exercises switch-account and the token gets re-scoped. - Three GET overloads = a small Strategy: authenticated (token), public (no auth), and API-key calls share one path but differ in headers. - Multi-version surface (v1–v4). Base URIs in config.properties so a test can hit the right API version without hard-coding. - Contract testing via JSON Schema. 19+ schemas mean tests fail when the response shape drifts, not just when a single field is wrong β€” catches breaking API changes early. - Bulk data via POI/OpenCSV for CSV/Excel upload flows (e.g., bulk content/quest import). - ResponseDataHelper threads IDs from one response into the next request β€” chaining signup β†’ verify-OTP β†’ create-account without hard-coded IDs.

Q&A

Q1. Why build it from scratch instead of Postman/Newman?

"Postman is great for exploration, but I needed it inside CI with real assertions, JSON-schema contract checks, response chaining, and profile-switch logic that Postman scripting gets messy at. A Java + Rest Assured + TestNG framework gave me code reuse via helpers, schema validation, and mvn clean test integration. Postman stayed as the exploratory and documentation tool; the framework is the regression gate."

Q2. How do you handle auth and the multi-profile model?

"LoginHelper does signup β†’ verify-OTP β†’ login and gets a bearer token, which AccessTokenHelper caches. Because a Questt account can have a student and a teacher profile, SwitchAccountHelper calls switch-account and AccessTokenHelper re-scopes the token to the active profile. So a single test can create an account, switch to the teacher profile, and assert teacher-only Navigator endpoints with the right token."

Q3. What are the three GET overloads for?

"Same endpoint, different auth context. One GET injects the bearer token for authenticated calls, one sends no auth for public/without-login endpoints, and one sends an API key for service-to-service style calls. Centralizing them means the test just picks the right overload instead of re-building headers each time."

Q4. How do you validate responses beyond status code?

"Two levels. Field-level β€” pull values with jsonPath() and assert business expectations from JSON test data. And contract-level β€” Rest Assured's matchesJsonSchema against 19-plus schema files, so if the API changes a field type or drops a key, the test fails on the shape, not just on a missing value. That schema layer is what catches silent breaking changes between API v-versions."

Q5. How is test data managed and how do you chain requests?

"ReadTestData reads JSON request bodies and expected values, so data is external to logic. ResponseDataHelper extracts IDs from one response and feeds the next β€” for example the account ID from create-account flows into the profile-CRUD calls β€” so I never hard-code IDs and the flow mirrors real usage."

Q6. How does it run and what's missing on the CI side?

"mvn clean test against questt_API.xml, which has 7 active sub-suites. Honestly it's not wired into Jenkins yet β€” it runs locally and on demand. Native TestNG reports, no ExtentReports. Wiring CI and richer reporting is my top backlog item."

STAR β€” schema layer caught a breaking change

  • S: Questt's backend spans API v1-v4; a v-bump quietly changed a response field type on a Navigator endpoint.
  • T: Catch contract drift before it reached the mobile app, without manually eyeballing every field.
  • A: I'd built JSON-schema validation into the response helpers β€” 19+ schemas. The next run failed on schema mismatch, not on a vague assertion.
  • R: The breaking change was flagged the same day at the contract level; backend reverted the type. It made the case for keeping schema validation as a standard layer, not an afterthought.

Honest caveats (volunteer these)

  • A real apiKey is committed in config.properties β€” I'd rotate it immediately and move secrets to env vars / a secret store. (This is the one to lead with β€” it shows security awareness.)
  • No Jenkins/CI yet β€” runs via mvn clean test; wiring a pipeline with smoke-on-PR + nightly regression is next.
  • TestNG native reports only β€” no ExtentReports; richer HTML + trend reporting is a quick upgrade.
  • Some flows depend on a fixed test OTP in stage β€” would break against real OTP delivery; a backend test-mode hook is the clean fix.

5.3 PROJECT DEEP-DIVE (B): Avysh B2BProjectTest (Selenium + Rest Assured, Java)

Path: /Users/rohan/ROhan personal/automation/B2BProjectTest β€” Avysh QA Engineer role (Sep 2020 – Aug 2022). Selenium 4 (alpha-5) + TestNG 7 + Rest Assured 4.4 + ExtentReports v2, Maven.

Pitch (30s)

"B2BProjectTest is the regression and smoke framework I built at Avysh β€” a multi-tenant B2B e-commerce and PIM platform where brands publish products, sellers resell them, and buyers order through storefronts. It's Java with Selenium 4, TestNG, and Rest Assured for the order APIs. The headline is a three-layer design β€” tests call helper classes, helpers orchestrate page objects, and page objects extend a CommonUtils base β€” so tests read like business flows, not locator soup. It's JSON-data-driven, Angular-aware via ngwebdriver, auto-retries failed tests once through a TestNG listener, and produces custom ExtentReports with screenshot-on-failure."

Architecture / design

src/test/java/com/avysh/qa/module/...   TEST classes
        β”‚ calls
src/main/java/.../helper/...             HELPER / FACADE (24 classes) β€” business flows
        β”‚ orchestrates
src/main/java/.../pages/...              PAGE OBJECTS (44 classes) β€” @FindBy + PageFactory
        β”‚ extend
util/CommonUtils                         BASE β€” click/sendKeys wrappers, waits, Angular sync,
                                         alerts, JS executor
- Three-layer separation (the headline pattern): test β†’ helper β†’ page β†’ CommonUtils. A test reads "login, add a product, verify it." - POM + PageFactory: 44 page classes, @FindBy + PageFactory.initElements. - Angular-aware sync: ngwebdriver's waitForAngularRequestsToFinish() β€” plain Selenium waits don't know Angular's digest cycle, so this killed a class of timing flakiness. Plus explicit WebDriverWait and document.readyState polling. - Data-driven via JSON (Gson), not DataProvider: ReadTestData returns an ordered LinkedHashMap for UI, JsonObject for API. - AssertJ SoftAssertions: report all field mismatches per run, not just the first. - Listeners: ExtentReporterNG implements IReporter; Retry implements IAnnotationTransformer auto-attaches a 1-retry RetryAnalyzer to every test β€” no per-test annotation. - Driver factory: WebDriverUtils (Chrome/Firefox/Edge/IE/Safari + headless), holds a ThreadLocal<RemoteWebDriver>; suites run parallel="classes" (thread-count 1-4). - Module suites: Smoke, brandPIM, sellerPIM, orderAPI, integration β€” CI runs only what a change touches; smoke stays minutes-fast, regression runs nightly. - Custom exception layer: captures screenshots for Selenium exceptions, formats HTML traces that label "Script Issue" vs "Application Issue" for fast triage.

Q&A

Q1. Why a helper layer on top of POM β€” isn't POM enough?

"POM gives page-level actions, but a real flow spans many pages β€” login, navigate, fill a multi-step form, verify. Putting orchestration in helper/facade classes keeps tests declarative and lets several tests reuse the same flow, like LoginHelper.loginToApplication(). It's a facade over the page objects."

Q2. The app is Angular β€” how do you handle synchronization?

"Plain Selenium waits don't understand Angular's async digest, so elements look present before data binds. I use ngwebdriver's waitForAngularRequestsToFinish() around actions to wait for Angular to be stable, plus explicit WebDriverWait with ExpectedConditions, and a document.readyState poll for full loads. That combo removed most timing flakiness. Honest note β€” there are still a few Thread.sleep hard waits I'd replace."

Q3. Why AssertJ SoftAssertions over TestNG hard asserts?

"A hard assert stops at the first failure, so you fix-and-rerun one field at a time. SoftAssertions collect every failure and report them at assertAll() with .describedAs() messages. Verifying a 10-field product, I see all mismatches in one run β€” much faster feedback."

Q4. How does parallel execution and the driver work?

"WebDriverUtils is the factory and holds a ThreadLocal<RemoteWebDriver>; getDriver() returns the thread-local if set. Suites declare parallel='classes' with thread-counts up to 4, so each class runs in its own thread with its own driver. Honest caveat β€” the full ThreadLocal/Grid hubURL path is declared but not fully exercised; for real parallel-at-scale I'd centralize the driver pool and wire Grid properly."

Q5. How do the Rest Assured API tests integrate with UI?

"OrdersAPI wraps Rest Assured, OrdersAPIHelper builds payloads by templating {orderId}/{status} into JSON. Two uses β€” setup, where I postOrder() to get an order ID in milliseconds instead of clicking through the UI, then drive UI to verify; and validation, where I create via UI then hit the API to confirm the backend state, including negatives like 403, INSUFFICIENT_API_PARAMETERS, ORDER_NOT_FOUND. Honest note β€” those API tests are currently commented out in the suite; the coverage exists but is disabled."

Q6. How does failure diagnosis work?

"Custom ExtentReporterNG produces timestamped HTML with TestNG groups as categories. On failure, my CustomException layer captures a screenshot for Selenium exceptions and formats an HTML trace labeling Script Issue vs Application Issue β€” a reviewer instantly knows whether the test or the app broke."

STAR β€” Angular flakiness

  • S: Avysh's Angular front end made Selenium tests intermittently fail β€” elements present before data bound.
  • T: Build a maintainable suite that wasn't flaky on an async SPA, covering UI and order APIs.
  • A: Three-layer POM, ngwebdriver Angular sync around actions, JSON data-driving, a Rest Assured layer with negative cases, a 1-retry listener, and the screenshot + script-vs-app exception layer.
  • R: Tests read as business flows, locator changes isolate to one class, Angular flakiness dropped sharply, and failures arrive with a screenshot and a clear triage label.

Honest caveats

  • No shared BaseTest β†’ duplicated @BeforeClass/@AfterClass. No logging framework (System.out.println/printStackTrace). Plaintext credentials in config.properties. WebDriverManager declared but unused (bundled binaries). Grid/parallel-remote path incomplete. Selenium 4.0.0-alpha-5 / ExtentReports 2.41 (2016) / Java 1.8 β€” all want bumping to stable/LTS. enviroment typo across suites. ~25 tests (incl. all of OrdersAPITest) commented out.
  • Pro framing: lead with strengths (three-layer POM, Angular sync, soft assertions, custom reporting), then volunteer 2-3 of these as "known tech-debt I'd refactor next β€” BaseTest, logging, secrets first."

5.4 PROJECT DEEP-DIVES (C): Brief β€” AI/LLM Eval Framework & Morrie Playwright

C1. LLM Evaluation Framework (Python, pytest, BKG-driven, agentic oracle)

Path: /Users/rohan/questt/bkg-chat-eval/chat-eval/ (canonical; FMCG + HOAD are siblings)

Pitch (30s):

"A project-agnostic evaluation framework for analytics AI chatbots. Instead of grading an answer with one LLM judge, we grade against six independent signals β€” three deterministic. The standout is an oracle evaluator that re-runs the agent's own SQL tool-calls against the live DB to get ground truth, a BKG evaluator that checks whether the bot picked the right tables and KPIs from a Business Knowledge Graph, and a regex safety guard. The other three β€” content quality, on-topic, latency β€” use LLM-as-judge with business context injected from the BKG. The aggregator combines them with weighted scoring and three hard gates: safety, oracle-when-tool-calls-present, and an overall threshold. Project-agnostic β€” drop in a new BKG JSON and golden dataset, the code doesn't change. Python, pytest, asyncio, Playwright for the UI channel, Langfuse for traces."

Architecture beats: - Adapter pattern β€” API and UI adapters return the same normalized payload, so evaluators don't care which channel produced the answer. Adding gRPC/WebSocket = adding an adapter. - Two-tier scoring β€” cheap deterministic checks short-circuit the expensive LLM judge (if BKG hard-fails, skip the judge β†’ cost saving). - BKG as oracle β€” L1 graph ships a python_function per KPI; BKGKPIRunner execs it against the live DB for ground truth β†’ judge is reference-implementation-aware, not just opinion. - Weight profiles β€” weights when expected_answer present; weights_bare shifts content_quality's weight onto oracle/bkg/on_topic when there's no ground truth (Strategy pattern). - HOAD evaluate_sql.py β€” pairs each final_query with its query_executor_tool sub-question, validates SQL against BKG (sqlglot syntax + unknown tables/columns/joins), 60/40 deterministic-favored blend with the judge, min-score across legs so the weakest sub-query gates the verdict.

The six evaluators (know the table):

Evaluator Threshold What it does
oracle 0.70 Re-runs the agent's own tool_results against the live DB for ground truth, then compares. Hard-gated when tool_results present.
bkg 0.50 Schema/entity grounding via the BKG L1 graph β€” right table/KPI selected?
content_quality 0.60 LLM judge β€” "did you produce the right answer shape?" (merged 4 old judges).
on_topic 0.50 LLM judge β€” did the response stay in scope? (merged 2 old judges).
safety 0.80 Regex PII + prompt-injection guard. Hard-gated.
latency warn 15s / fail 60s SLA classification + 0-1 score.

Three hard gates (any one fails the whole test): safety < threshold; oracle < threshold AND tool_results present; overall < 0.60.

Likely Q&A: - Why six evaluators not one? β€” "LLM-as-judge alone gets fooled by a confidently-stated wrong number. Layering deterministic ground-truth (oracle re-runs the SQL, BKG validates schema choice) cuts false positives and costs less because deterministic checks short-circuit the LLM call." - How do you test something non-deterministic? β€” "I separate what's deterministic from what isn't. Schema selection and the numeric answer are deterministic β€” I check them against the BKG and a live-DB oracle. Only genuinely subjective things β€” was it on topic, right answer shape β€” go to the LLM judge. And I gate hard on safety and oracle." - How do you keep LLM-judge cost down? β€” "Two ways. Deterministic short-circuiting β€” if BKG validation finds the SQL references non-existent tables, skip the LLM call entirely. And consolidation β€” I merged eight overlapping judges into two, dropping per-case LLM cost ~75% with no loss of signal." - What's the BKG? β€” "Business Knowledge Graph β€” a session document of company facts plus an L1 graph of KPI/entity nodes. Each KPI node ships a reference python_function; my evaluator execs it against the live DB so I have an independent ground-truth number, not just an LLM's opinion." - Caveat: "Three near-identical forks (chat-eval/FMCG/HOAD) accumulate drift β€” the right fix is a published chat-eval-core package each project imports."

C2. Morrie β€” Questt.ai (Playwright + TypeScript)

Path: /Users/rohan/questt/Morrie_automation/playwright-automation/

Pitch (30s):

"The Playwright + TypeScript framework for Morrie, our chatbot product, deployed via Jenkins. Combined UI + API testing. The key decision is that OTP auth runs once in a global setup, not per test β€” API leg posts email to /auth/login, posts email+OTP to /auth/verify-otp, saves the access token; UI leg drives the sign-in screen and saves browser storage state. Then two Playwright projects β€” web/e2e reuse the storage state so they never see a login screen, API uses the env token. Reporters are HTML, JSON, JUnit; CI is a parameterized Jenkinsfile with a TEST_SUITE choice."

Architecture beats: - Global setup as the auth boundary β€” login once, saves ~5-10s Γ— hundreds of tests. - pressSequentially over fill on the email field β€” the React form validates on keystroke events; fill sets the value via DOM and skips them, leaving Continue disabled. (A real Playwright gotcha I found and fixed; added blur() + expect(...).toBeEnabled().) - Two Playwright projects β€” web/e2e get storageState, API skips the browser entirely (faster). - BaseApi (Axios wrapper) centralizes base URL + auth header, spaces requests and retries on HTTP 429 so OTP/login throttling doesn't flake the suite. - Config: retries: isCI ? 1 : 0, workers: isCI ? 2 : undefined, forbidOnly: isCI, screenshot/video/trace on failure.

Likely Q&A: - Why pressSequentially? (see above β€” best "I found a real bug" story for Playwright). - Why split API and web projects? β€” "Web/e2e need browser cookies (storage state); API needs only the bearer token. Two projects keep configs clean and the API project runs much faster without Chromium." - Caveat: "Storage state can expire mid-run; only Chromium today (no Firefox/WebKit); TEST_OTP is hard-coded for the sandbox."


5.5 PROCESS β€” SDLC, STLC, Agile, Defects, Artifacts, Test Types

SDLC (PARDIM) & models

Planning β†’ Analysis β†’ Requirement/Design β†’ Development β†’ Implementation(Testing) β†’ Maintenance. Models: Waterfall (fixed reqs, govt), V-Model (each dev phase has a parallel test phase, banking/healthcare), Agile (frequent change), Spiral (high-risk), DevOps (CI/CD daily releases).

STLC (6 phases)

  1. Requirement Analysis β†’ 2. Test Planning β†’ 3. Test Case Design β†’ 4. Test Environment Setup β†’ 5. Test Execution (log + retest defects) β†’ 6. Test Closure (reports, metrics, lessons).
  2. Entry: build available, env ready, data prepared. Exit: all planned tests run, critical bugs closed, summary signed off.

Agile / Scrum (3-5-3) and my role

  • 3 roles: Product Owner, Scrum Master, Dev Team (incl. QA).
  • 5 ceremonies: Sprint Planning, Daily Standup, Sprint Review (demo), Retrospective, Backlog Refinement.
  • 3 artifacts: Product Backlog, Sprint Backlog, Increment.
  • My role as SDET in Scrum: "In planning I size testing effort and flag untestable stories; in refinement I add acceptance criteria and edge cases so a story is testable before it's pulled in; daily standup I raise blockers (env down, build not deployed); during the sprint I write automation alongside dev so it's done by Definition of Done, not a sprint behind; in review I demo coverage; in retro I raise process issues like flaky CI. I treat QA as shift-left β€” testable specs and automation in-sprint, not a gate at the end."
  • DoD includes: code reviewed, unit tests pass, QA tested, deployed to staging, docs updated.

Defect lifecycle

New β†’ Assigned β†’ Open β†’ Fixed β†’ Retest β†’ Verified β†’ Closed. Other states: Rejected, Deferred, Duplicate, Reopened, Not a Bug. Bug report must-haves: clear title, steps to reproduce, expected vs actual, severity, priority, environment (OS/browser/build), screenshot/video/logs, reproducibility.

Severity vs Priority (the classic β€” give an example each)

  • Severity = how bad (impact). Priority = how soon (urgency).
Severity Priority Example
High High Login broken on prod β€” blocks everyone now
High Low Crash in a rarely used legacy module β€” bad but few hit it
Low High Company logo wrong / typo in product name on homepage β€” cosmetic but visible to all, fix fast
Low Low Typo in footer

Test artifacts

Test Strategy (org-wide) β†’ Test Plan (project scope/schedule/resources) β†’ Test Scenario (high-level "what") β†’ Test Case (detailed "how") β†’ RTM (maps requirements ↔ test cases, proves coverage) β†’ Test Summary Report. Test-case design techniques: Equivalence Partitioning, Boundary Value Analysis, Decision Table, State Transition, Use Case, Error Guessing.

Types of testing (be ready to define crisp)

Smoke (build boots?) vs Sanity (does the fix work, no new breaks?); Regression (old features still work after change); Functional, Integration, System, UAT, Exploratory, Ad-hoc, Performance, Security (OWASP), Usability, Compatibility, Accessibility (WCAG). - Verification vs Validation: "building it right" (reviews, before code) vs "building the right thing" (testing, after code). - Re-test vs Regression: verify one specific fix vs ensure broader features still work. - Smoke vs Sanity (tricky): smoke = whole app, shallow, after a new build, usually documented; sanity = specific area, deep, after a bug fix, usually not documented.

Key metrics (have formulas ready)

Defect Density = defects/KLOC. Defect Leakage = prod defects / total defects. Test Case Pass% = passed/executed Γ—100. DRE (Defect Removal Efficiency) = found-before-release / total Γ—100. Test Coverage = tested reqs / total reqs. Automation Coverage = automated cases / total cases.

Quick-fire definitions

  • Hotfix: emergency fix straight to prod. Shift-left: test earlier in SDLC (during dev). Risk-based testing: prioritize by business impact Γ— failure likelihood. TDD: test first, then code. BDD: Given/When/Then behavior specs (Cucumber/Gherkin).
  • SDET vs Automation Engineer vs Manual QA: SDET owns framework design + CI/CD + can do perf/white-box; automation engineer writes scripts; manual QA executes by hand. I position as SDET β€” I design frameworks, own CI, and code helpers/tools, not just tests.

5.6 REAL-TIME SCENARIOS (with answers)

1. A test is flaky β€” passes sometimes, fails sometimes.

"First I stop trusting it and quarantine it so it doesn't block the pipeline. Then I reproduce by running it in a loop locally. Flakiness is almost always sync, test data, or order-dependence. I check for hard waits and replace them with explicit/auto-waits, check for shared state between tests, and check whether setup races the app. I fix the root cause, not just add retries β€” retries are a safety net, not a cure. Only after it's green N times in a loop do I take it out of quarantine."

2. Test passes locally but fails in CI.

"Classic environment-difference problem. I check the obvious axes β€” headless vs headed rendering, screen size/viewport, timezone/locale, slower CI machines needing longer waits, different test data or a stale build on the agent, and parallelism (CI runs workers, local runs serial, so a hidden shared-state bug surfaces). I reproduce CI conditions locally β€” run headless with CI's worker count β€” and add the missing explicit wait or isolate the shared state. In Morrie I set retries: isCI ? 1 : 0 precisely because CI is a different beast, but I still treat a CI-only failure as a real bug to root-cause."

3. A critical bug is found late, just before release.

"I don't sit on it β€” I report it immediately with severity, priority, and clear repro so the call can be made fast. Then it's a risk decision, not mine alone: I lay out impact, blast radius, and whether there's a workaround for the PM/release owner. Options are fix-and-reslip, hotfix-after-release, or ship-with-known-issue-and-document. My job is to make the risk visible with evidence; the go/no-go is the team's. If we ship around it, I write the workaround and a fast-follow ticket."

4. Developer says 'it's not a bug.'

"I treat it as a gap in shared understanding, not a fight. I show the exact steps, logs, and expected-vs-actual, and I tie expected behavior back to the requirement or acceptance criteria. If it's genuinely ambiguous spec, I pull in the PO/BA to clarify the intended behavior β€” the spec is the tiebreaker, not opinion. Often it turns out to be a requirements gap, and then we fix the spec, not just the code. If we still disagree on a real defect, I escalate with the evidence rather than letting it die silently."

5. No time to automate this sprint.

"I prioritize by risk. The critical happy paths and anything high-traffic get automated first; low-risk edge cases get a manual checklist for now and an automation ticket in the backlog. I'd rather have reliable automation on the 20% of flows that carry 80% of the risk than rushed, flaky coverage of everything. I also lean on the API layer β€” API tests are faster to write and more stable than UI, so I get coverage there first and add UI E2E when there's time."

6. The whole suite is slow and devs ignore it.

"Speed is a feature for test suites. I split into a fast smoke set that runs on every PR in single-digit minutes and a full regression that runs nightly β€” that's exactly why the Avysh suites are module-scoped. I push coverage down the pyramid: more API tests, fewer heavy UI E2E. I parallelize (parallel='classes'/xdist/Playwright workers) and use API calls for setup instead of clicking through the UI. If devs trust a fast, reliable smoke gate, they stop ignoring it."

7. Requirements keep changing mid-sprint.

"Expected in Agile. I keep tests loosely coupled to specifics β€” data-driven inputs and POM/helper layers mean a changed field touches one place. I delay writing brittle detailed cases until the story stabilizes, focus on acceptance criteria, and use refinement to catch changes early. When a requirement flips, I update the RTM so coverage stays honest."

8. How do you test an AI/LLM feature where output isn't fixed?

"I separate deterministic from non-deterministic. Anything checkable against ground truth β€” the number, the schema, the SQL the agent ran β€” I verify deterministically with an oracle and a knowledge-graph check. Only genuinely subjective qualities β€” on-topic, answer shape β€” go to an LLM-as-judge, and I hard-gate on safety. That's exactly the six-evaluator design in my Questt eval framework: fewer false positives than a single judge, and it costs less because deterministic checks short-circuit the expensive LLM call."

9. "How would you test a login page?" (the guaranteed warm-up)

"I cover it across layers. Functional positive β€” valid creds log in, redirect to dashboard. Negative β€” wrong password, non-existent user, empty fields, locked account, expired OTP. Validation β€” email format, field-level errors, required-field messages. Security β€” SQL injection in the fields, XSS, brute-force lockout, password masking, no creds in the URL or logs, session token on HTTPS only, session expiry and logout. UI/UX β€” tab order, enter-to-submit, error message clarity, remember-me. Compatibility β€” cross-browser and mobile viewport. And the OTP path specifically, since my Questt and Avysh apps use mobile+OTP β€” resend, wrong OTP, expiry, rate-limiting. I'd automate the happy path and the top negatives in API + UI, and keep edge cases as exploratory."


5.7 BEHAVIORAL β€” STAR Themes + Infosys HR

STAR = Situation, Task, Action (what YOU did), Result (numbers if possible). Have a 2-min story per theme, practiced out loud.

Theme One-line STAR hook (from my real work)
Challenging bug / flaky test Flaky test passed local, failed CI 30% β€” added retry instrumentation + logs, found a race in API setup, refactored to Playwright fixtures with explicit waits β†’ flakiness <1%.
Built something from scratch Built the Questt Rest Assured API framework from zero β€” 62 tests, 15 classes, JSON-schema contract validation across API v1-v4 β†’ a contract change caught same-day.
Caught a serious defect VAPT IDOR-style finding β€” replayed a captured request, got cross-tenant financial data; reported with exact repro, dev added tenant_id checks, retest confirmed clean.
Improved a process / drove ownership Designed module-scoped TestNG suites + smoke-on-PR vs nightly regression so CI ran only relevant tests β†’ faster feedback, devs stopped ignoring the gate.
Conflict / influencing without authority "Not a bug" disagreement β€” tied expected behavior to acceptance criteria, pulled in the PO; turned out to be a spec gap, fixed the requirement and the code.

Full STAR β€” flaky CI test (memorize, it covers theme 1):

S: At Questt we had a test that passed locally but failed in CI about 30% of the time, blocking a release. T: Find the root cause fast so the release wasn't held hostage to a flaky gate. A: I quarantined it so it stopped blocking, added retry instrumentation and extra logging, and reproduced it by looping it under CI's worker count. It was a race in the API setup β€” the test started before auth settled. I refactored to Playwright fixtures with explicit waits and moved auth into global setup. R: Flakiness dropped to under 1%, the gate became trustworthy again, and release confidence improved.

Full STAR β€” serious security finding (covers theme 3):

S: During a VAPT engagement on a Questt-stack reporting API. T: Verify object-level authorization on a financial endpoint. A: I captured a GET /api/v1/daily-report in Burp with my own bearer token, replayed it altering the user context, and got back another tenant's financial data β€” an IDOR / OWASP A01 Broken Access Control issue. R: I reported it with the exact request, the cross-tenant response, and a repro recipe. Dev added tenant_id verification at the service layer; my retest scan confirmed clean. It's why I always pair authenticated DAST with manual replay β€” scanners catch protocol issues, not authorization logic.

Infosys HR questions β€” how to answer

Why Infosys?

"Infosys works across domains at scale β€” banking, healthcare, retail β€” and is investing heavily in AI and automation through Topaz and its platforms. My profile is exactly that intersection: traditional SDET work in Java/Selenium and Playwright, plus hands-on LLM-evaluation frameworks, which is rare. A lateral SDET role here lets me bring framework-design depth and apply the AI-testing skill across many client domains rather than one product." (Keep it about scale, domain breadth, and the AI direction β€” not generic praise.)

Why are you leaving / looking?

"Growth and breadth. At Questt I built six frameworks across automation, AI evaluation, and security testing, mostly owning each end-to-end. I want to apply that at larger scale and across more domains, and to work in a stronger engineering-process environment with mature CI/CD and mentorship. It's a pull toward a bigger stage, not a push away." (Never criticize the current employer.)

Strengths?

"Framework design from first principles β€” I can stand up a maintainable, layered framework in Java or TypeScript or Python and defend every architectural choice. And I'm honest about tech debt β€” I lead with strengths and volunteer what I'd refactor, which keeps frameworks healthy." (Pick strengths you can prove with the projects above.)

Weakness?

"I've tended to under-invest in CI wiring early β€” my Questt API framework ran on mvn clean test longer than it should have before I prioritized a pipeline. I've corrected by making CI integration a day-one backlog item now, and I'm actively deepening Jenkins/GitHub Actions config." (Real, specific, with a correction β€” not a humblebrag.)

Notice period?

"My notice is [X days/2 months]. I can also explore an early release / buyout if the timeline needs it." (State it plainly, show flexibility.)

Current & expected CTC?

"My current CTC is [X]. For this role I'm looking at [Y], which reflects the lateral SDET level plus my AI-testing skill set, but I'm open to discussing the full package." (Give a range, anchor to the role + rare skill, stay open.)

General HR delivery rules: stay positive about the past employer, keep answers 60-90s, back claims with a project, and end on what you bring to Infosys. Confidence and a clear explanation beat a perfect-but-hesitant answer.


6. Rapid-Fire Revision (read the last 30 minutes)

  • Python: mutable vs immutable; is vs ==; comprehension vs generator; decorator (wraps a fn to add behavior); context manager (with β†’ guaranteed cleanup); *args/**kwargs; shallow vs deep copy.
  • Selenium: 3 waits + why explicit; StaleElementReference β†’ re-find; CSS vs XPath; JS click when overlapped; POM; time.sleep is not a wait.
  • Rest Assured: given/when/then; RequestSpec; POJO + Jackson; JsonPath; JSON-schema validation; 401 vs 403; extract() β†’ chain.
  • OOP: 4 pillars + a real example each; equals/hashCode contract; overloading vs overriding; ArrayList vs HashMap.
  • SQL: 2nd-highest salary (DENSE_RANK or subquery); joins; GROUP BY ... HAVING; WHERE vs HAVING.
  • Framework: layers β†’ tools β†’ data β†’ reporting β†’ CI β†’ how a single test runs.
  • Per project: one metric + one trade-off + one "what I'd improve."

3-Day Crunch Plan (~16–18 hrs)

  • Day 1 (6h): Python revise (2.5h) + code by hand (1.5h) + Selenium (2h)
  • Day 2 (6h): Rest Assured (2h) + OOP/Collections (1.5h) + SQL by hand (1.5h) + framework (1h)
  • Day 3 (5–6h): re-read your 2 repos (2h) + mock out loud (2h) + scenarios/behavioral/HR (1h) + skim this doc (1h)
  • Morning of: Section 6 only (30 min)

Highest-yield: write code/SQL by hand, and know your own projects cold.

Deeper references (only if a follow-up goes beyond this doc)

Selenium β†’ 05 Β· Rest Assured β†’ 09 Β· Python/Java deep β†’ 13 Β· pytest/TestNG/RA patterns β†’ 15 Β· SQL β†’ 18 Β· Project Q&A β†’ 14 + Project_QnA/ Β· Coding β†’ 17 Β· Real Qs β†’ 10/11