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)
"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):
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:
| 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."
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
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:
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
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 |
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
@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
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 ""))
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)¶
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
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:
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", {})
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.responseReceivedevent. Nevertime.sleepas 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
WebElementreferences. StoreBylocators and re-resolve on every access, or use a FluentWait that ignoresStaleElementReferenceExceptionand retries the action.
Q5. File upload with no visible <input> (drag-drop uploader)?
If a hidden
<input type=file>exists,send_keysthe absolute path β works even when hidden. If there's truly no input, inject a File via JS and dispatch adropevent.
Q6. Open a link in a new tab and verify its content, then return?
Save
current_window_handle, trigger the new tab (orswitch_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-dirChrome profile.
Q8. A dropdown won't work with the Select class β why?
Selectonly 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.requestPausedand 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 fireinput/change. Usesend_keys(Keys.CONTROL,"a")thensend_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 pytestpytest_runtest_makereporthook (orITestListener.onTestFailurein 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, thenswitch_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
Bylocators (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/ TestNGparallel). 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 orpytest-xdistsharding 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"));
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
}
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))orbody(\"data.order.items[0].sku\", equalTo('SKU-1')). For structure-wide correctness I addmatchesJsonSchemaInClasspathso 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/@BeforeSuiteand 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.baseURItrap). Instance-scoped specs. Each test creates and deletes its own data. TestNGparallel='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
@JsonPropertyonce 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-Matchetag, 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
@DataProviderreturningObject[][]β each row including the expected status β drives one@Testacross 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
balancewithout 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
Listinterface is abstraction;privatearray insideArrayListis 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()toCollection) without breaking every existing implementer. - Python note:
abc.ABC+@abstractmethodfor abstract classes; no pure interface keyword β ABCs ortyping.Protocolfill 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/finalmethods. - 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 viahashCode(), then confirm withequals(). Override one without the other andset.contains(equalObject)returns false β silent data loss. - equals properties: reflexive, symmetric, transitive, consistent, and
x.equals(null)==false. - Follow-up β "
instanceofvsgetClass()?"instanceoflets 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__,isis 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
ArrayDequeoverLinkedListfor 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+)¶
-
put(k,v)computeshash(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 withequals(); 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. -
Complexity: O(1) average get/put; O(log n) worst with treeification; O(n) only with a pathological
hashCode(). - 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 amodCount; structural modification during iteration throwsConcurrentModificationException. Use the iterator'sremove()or aremoveIf()instead. - Fail-safe (
CopyOnWriteArrayList,ConcurrentHashMap): iterate over a snapshot/segment, no exception, may not see latest writes. - Python note: mutating a
dict/listmid-iteration raisesRuntimeError/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
throwsortry/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/finallygotcha: areturninfinallyswallows exceptions and overrides earlier returns β avoid it.- Follow-up β "Why do many devs dislike checked exceptions?" Verbose
throwschains; they don't compose with lambdas. Modern frameworks (Spring) lean unchecked. - Python note: all exceptions are unchecked;
try/except/else/finally; custom viaclass 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;itertoolsfor 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 ifa>b. - Follow-up β "Should compareTo be consistent with equals?" Strongly recommended; inconsistency corrupts
TreeMap/TreeSetbehavior. - 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
staticblock runs once at class load;staticmethods can't usethis/instance fields.- Follow-up β "
finalon a reference?" The reference can't be reassigned, but the object's contents can still change (final Listcan 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 inON. - MySQL note: no
FULL OUTER JOINβ emulate withLEFT 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¶
- 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 frameROWS 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 INwith a NULL in the subquery returns no rows β a classic bug; preferNOT 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_nameout oforder_items). - 3NF: 2NF + no transitive dependency (
dept_namedepends ondept_id, so it belongs indepartments, notemployees). - 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 onA,A+B,A+B+Cβ notB,C, orB+Calone. - Follow-up β "How to verify index use?"
EXPLAIN; watch fortype=ALL(full scan) andUsing 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
Q2. Department with highest average salarySELECT 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;
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) rn FROM orders
) t WHERE rn = 1;
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;
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));
SELECT MONTH(created_at) m,
SUM(status='COMPLETED') completed,
SUM(status='PENDING') pending
FROM orders GROUP BY MONTH(created_at); -- MySQL: boolean -> 1/0
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;
_idis an autoObjectId. 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 viaReadTestData; Avysh used Gson JSON (orderedLinkedHashMapfor UI,JsonObjectfor 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 offmainfor prod emergencies. - Typical flow I follow:
git checkout -b feature/Xβ commit small β push β open PR β CI runs smoke + lint β review β squash-merge β CI onmainruns 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, parameterizedTEST_SUITE= all/api/ui/e2e), GitHub Actions (HOAD evaleval_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 likeLoginHelper,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 withmvn 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)
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 testintegration. 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?
"
LoginHelperdoes signup β verify-OTP β login and gets a bearer token, whichAccessTokenHelpercaches. Because a Questt account can have a student and a teacher profile,SwitchAccountHelpercallsswitch-accountandAccessTokenHelperre-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'smatchesJsonSchemaagainst 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?
"
ReadTestDatareads JSON request bodies and expected values, so data is external to logic.ResponseDataHelperextracts 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 testagainstquestt_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
apiKeyis committed inconfig.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
CommonUtilsbase β 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
@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 explicitWebDriverWaitwithExpectedConditions, and adocument.readyStatepoll for full loads. That combo removed most timing flakiness. Honest note β there are still a fewThread.sleephard 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?
"
WebDriverUtilsis the factory and holds aThreadLocal<RemoteWebDriver>;getDriver()returns the thread-local if set. Suites declareparallel='classes'with thread-counts up to 4, so each class runs in its own thread with its own driver. Honest caveat β the full ThreadLocal/GridhubURLpath 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?
"
OrdersAPIwraps Rest Assured,OrdersAPIHelperbuilds payloads by templating{orderId}/{status}into JSON. Two uses β setup, where IpostOrder()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
ExtentReporterNGproduces timestamped HTML with TestNG groups as categories. On failure, myCustomExceptionlayer 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 inconfig.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.enviromenttypo across suites. ~25 tests (incl. all ofOrdersAPITest) 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 aTEST_SUITEchoice."
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)¶
- 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).
- 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 : 0precisely 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-reportin 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 testlonger 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;
isvs==; 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.sleepis 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_RANKor subquery); joins;GROUP BY ... HAVING;WHEREvsHAVING. - 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