SDET-Specific Programming Topics — Interview Depth¶
Companion to 13_Programming_Deep_Dive. That file covers general programming. This file covers the topics that actually get asked in SDET / QA Automation interviews at Bangalore product companies — Razorpay, PhonePe, Swiggy, Zomato, CRED, Postman, BrowserStack, Atlassian, Adobe, Salesforce, Microsoft, Google, Amazon, Flipkart, Walmart Labs, ServiceNow, Intuit, ThoughtWorks, plus services companies (TCS, Infosys, Wipro, EPAM).
Same format as file 13: What it is → Why interviewers ask → Code with explanation → Follow-up they'll ask → Wrong answer that gets you rejected.
What's covered¶
| Part | Topic | When it's asked |
|---|---|---|
| 1 | Pytest deep dive (fixtures, parametrize, conftest, markers) | Every Python SDET role |
| 2 | TestNG deep dive (listeners, DataProvider, dependsOn, retry) | Every Java SDET role |
| 3 | Playwright fixtures + TypeScript for tests | Modern UI SDET roles |
| 4 | REST Assured deep dive (specs, filters, POJOs, schema) | Every API SDET role |
| 5 | Selenium patterns (waits, Actions, JS executor, frames/windows) | Selenium roles |
| 6 | HTTP clients (Requests/httpx/Axios) — beyond the basics | Python + TS API testing |
| 7 | Mocking patterns (Mockito, unittest.mock, MSW, WireMock) | Mid–senior SDET |
| 8 | Page Object Model patterns (Base, Lazy, Fluent, Component) | UI automation roles |
| 9 | CI/CD scripting (Jenkinsfile, GitHub Actions, GitLab CI) | Every SDET-2 role |
| 10 | Test reporting (Allure, ExtentReports, Playwright HTML, JUnit XML) | Every SDET-2 role |
| 11 | Quick interview drill — 25 questions | Self-quiz |
PART 1: PYTEST DEEP DIVE¶
1.1 — Fixtures (the #1 pytest topic)¶
What¶
A fixture is a function decorated with @pytest.fixture that provides setup (and optionally teardown) to test functions. Tests request fixtures by name; pytest's DI system wires them up.
Why interviewers ask¶
Pytest fixtures are the difference between a brittle test suite and a maintainable one. Every Python SDET-2 interview probes fixture scopes, the yield pattern, and conftest.py propagation.
Basic fixture¶
import pytest
@pytest.fixture
def sample_user():
return {"id": 1, "name": "Rohan", "email": "r@x.com"}
def test_user_has_email(sample_user):
assert "@" in sample_user["email"]
sample_user is requested by parameter name
- pytest runs the fixture function and injects the return value
- New fixture instance per test by default (scope=function)
Fixture with setup + teardown (the yield pattern)¶
@pytest.fixture
def db_connection():
conn = psycopg2.connect("dbname=test") # setup
yield conn # value handed to test
conn.close() # teardown — runs after test
yield instead of return?
- Everything before yield is setup
- The yielded value is what the test receives
- Everything after yield runs after the test, even if the test fails
- Cleaner than addfinalizer or request.addfinalizer(cleanup)
Fixture scopes (memorize these)¶
| Scope | Created once per | Use case |
|---|---|---|
function |
each test (default) | Stateless setup, fresh data |
class |
each test class | Class-level helpers |
module |
each .py file |
Module-level DB seed |
package |
each package | Rarely used |
session |
entire pytest run | DB connection pool, Playwright browser |
@pytest.fixture(scope="session")
def db_engine():
engine = create_engine("postgresql://...")
yield engine
engine.dispose()
@pytest.fixture(scope="function")
def db_session(db_engine):
"""Each test gets fresh transaction; rollback at end."""
conn = db_engine.connect()
txn = conn.begin()
yield conn
txn.rollback()
conn.close()
conftest.py — fixtures shared across files¶
tests/
├── conftest.py # fixtures available to ALL tests under tests/
├── api/
│ ├── conftest.py # fixtures available to all tests under tests/api/
│ └── test_users.py
└── ui/
├── conftest.py # fixtures only for UI tests
└── test_login.py
conftest.py walking up the directory tree.
Follow-up they'll ask¶
- "What's the diff between scope=session and a module-level constant?" → Session fixture has setup + teardown lifecycle and can depend on other fixtures. A constant is just a constant.
- "Can a function-scope fixture use a session-scope fixture?" → Yes. Smaller scope can request larger scope. Reverse is illegal — pytest raises ScopeMismatch.
- "What if a fixture raises?" → The test errors (not fails). Tests dependent on the broken fixture are skipped.
- "What's
autouse=True?" → Fixture runs automatically without being requested. Useful for setup like setting random seed. Use sparingly — hidden side effects bite later.
Wrong answer that gets you rejected¶
"I use
@pytest.fixtureand put cleanup in atry/finallyinside the fixture." Why wrong: That works, but the idiomatic way isyield— cleaner, no nesting, plays nice withaddfinalizersemantics. Interviewers usingtry/finallysignals you haven't moved past unittest.TestCase mental model.
1.2 — parametrize (data-driven tests)¶
What¶
Run the same test multiple times with different inputs. Pytest's answer to TestNG's @DataProvider.
Basic parametrize¶
@pytest.mark.parametrize("email,expected", [
("a@x.com", True),
("invalid", False),
("", False),
("missing@", False),
])
def test_email_validator(email, expected):
assert is_valid_email(email) == expected
test_email_validator[a@x.com-True]
Multiple parametrize stacking¶
@pytest.mark.parametrize("browser", ["chromium", "firefox", "webkit"])
@pytest.mark.parametrize("locale", ["en-US", "hi-IN"])
def test_homepage(browser, locale):
# Runs 3 x 2 = 6 combinations
...
Indirect parametrize (pass through a fixture)¶
@pytest.fixture
def user(request):
role = request.param
return create_user(role=role)
@pytest.mark.parametrize("user", ["admin", "viewer"], indirect=True)
def test_dashboard(user):
# user is the OUTPUT of the fixture, not the role string
...
Follow-up they'll ask¶
- "How do you give test cases readable names?" → Use
ids=: - "Can you skip individual parametrized cases?" → Use
pytest.paramwith marks:
1.3 — Markers (selective execution)¶
Built-in markers¶
@pytest.mark.skip(reason="WIP")
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Need 3.10+")
@pytest.mark.xfail(reason="Known bug ABC-123") # expected to fail
@pytest.mark.parametrize(...)
Custom markers — for selective runs¶
# pytest.ini
[pytest]
markers =
smoke: minimal sanity tests
regression: full regression
slow: tests > 30s
db: requires database
pytest -m smoke
Run smoke AND db: pytest -m "smoke and db"
Run smoke NOT db: pytest -m "smoke and not db"
Follow-up they'll ask¶
- "What does
-kdo vs-m?" →-kmatches test name substring (e.g.,-k login).-mmatches marker. - "What's
--strict-markers?" → Errors on unknown markers (typos). Add topytest.ini.
1.4 — conftest.py and hooks¶
Common hooks¶
# conftest.py
def pytest_configure(config):
"""Run once at startup. Use for global state."""
config.addinivalue_line("markers", "slow: marks slow tests")
def pytest_collection_modifyitems(config, items):
"""Modify discovered tests. Common: skip slow on CI."""
if config.getoption("--no-slow"):
skip_marker = pytest.mark.skip(reason="slow test skipped")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_marker)
def pytest_runtest_makereport(item, call):
"""Hook each test phase. Common: capture screenshot on UI failure."""
if call.when == "call" and call.excinfo:
# test failed
page = item.funcargs.get("page")
if page:
page.screenshot(path=f"failures/{item.name}.png")
The request fixture — meta-info about current test¶
@pytest.fixture
def temp_dir(request):
path = tempfile.mkdtemp()
yield path
if request.node.rep_call.failed:
# keep dir for debugging if test failed
print(f"Test failed — kept dir at {path}")
else:
shutil.rmtree(path)
Wrong answer that gets you rejected¶
"I import fixtures from another file using
from helpers import my_fixture." Why wrong: That doesn't work — pytest discovers fixtures viaconftest.pypropagation, not Python import. The idiomatic answer is "put it in a conftest.py at the right level of the tree."
1.5 — Useful plugins to mention¶
| Plugin | What it does |
|---|---|
pytest-xdist |
Parallel test execution (pytest -n 4) |
pytest-rerunfailures |
Auto-retry flaky tests (--reruns 2) |
pytest-html |
HTML report |
pytest-cov |
Code coverage |
pytest-asyncio |
Async test support |
pytest-mock |
Cleaner mocker fixture wrapping unittest.mock |
pytest-bdd |
Gherkin-style BDD (rarely used in SDET-2) |
pytest-playwright |
Playwright fixtures for pytest |
PART 2: TESTNG DEEP DIVE¶
2.1 — Annotation execution order¶
What¶
TestNG runs annotated methods in a specific order. Knowing the order is mandatory.
The order (top to bottom for a single test class)¶
@BeforeSuite → once before all tests in the suite
@BeforeTest → before each <test> in testng.xml
@BeforeClass → once per test class
@BeforeMethod → before each @Test
@Test → the test
@AfterMethod → after each @Test
@AfterClass → once per test class
@AfterTest → after each <test>
@AfterSuite → once after all tests
Code with all hooks¶
public class BaseTest {
@BeforeSuite(alwaysRun = true)
public void suiteSetup() { System.out.println("Suite start"); }
@BeforeClass
public void classSetup() {
driver = new ChromeDriver();
}
@BeforeMethod
public void methodSetup(Method method) {
// method param gives access to test name
System.out.println("Starting: " + method.getName());
}
@AfterMethod
public void methodTeardown(ITestResult result) {
if (result.getStatus() == ITestResult.FAILURE) {
takeScreenshot(result.getName());
}
}
@AfterClass
public void classTeardown() { driver.quit(); }
}
Why interviewers ask¶
Wrong order = state leaks across tests = flaky suite. Tests this is one of the most reliable interview signals.
Follow-up they'll ask¶
- "Why
alwaysRun = true?" → Forces the method to run even if a group dependency failed. Use for teardown. - "What if I have BeforeMethod in BaseTest and BeforeMethod in child class?" → Both run. BaseTest first, then child. Inverse for
@AfterMethod(child first, then BaseTest).
2.2 — DataProvider (parametrized tests)¶
What¶
Java's answer to @parametrize. Supplies test data from a method.
Basic¶
@DataProvider(name = "loginData")
public Object[][] loginData() {
return new Object[][] {
{"valid@x.com", "Test@123", true},
{"invalid", "wrong", false},
{"", "", false},
};
}
@Test(dataProvider = "loginData")
public void testLogin(String email, String password, boolean expected) {
boolean actual = login(email, password);
Assert.assertEquals(actual, expected);
}
Read data from external source (CSV/JSON/Excel)¶
@DataProvider(name = "csvData")
public Object[][] csvData() throws IOException {
List<String[]> rows = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader("data/users.csv"))) {
String line;
br.readLine(); // skip header
while ((line = br.readLine()) != null) {
rows.add(line.split(","));
}
}
return rows.toArray(new Object[0][]);
}
Parallel DataProvider¶
@DataProvider(name = "users", parallel = true)
public Object[][] users() { ... }
// All rows run in parallel — each in its own thread
Follow-up they'll ask¶
- "Difference between
@DataProviderand<parameter>in testng.xml?" → Parameter is one value per<test>tag (suite-wide config like browser). DataProvider gives multiple data sets to one method. - "Where do you store test data?" → Small data inline. Medium data in JSON/YAML. Large data in DB or Excel via Apache POI.
2.3 — Listeners (the integration point for reports + retry)¶
What¶
Listeners hook into TestNG's lifecycle events. The pattern your B2BProjectTest uses — ExtentReporterNG and Retry are both registered as listeners in Smoke_Testng.xml.
The big listener interfaces¶
| Interface | When it fires |
|---|---|
ITestListener |
Before/after each test method (onTestStart, onTestSuccess, onTestFailure, onTestSkipped) |
ISuiteListener |
Before/after suite (onStart, onFinish) |
IInvokedMethodListener |
Before/after every method invocation (including config methods) |
IRetryAnalyzer |
Decides whether to retry a failed test |
IAnnotationTransformer |
Modify annotations at runtime — used to auto-apply RetryAnalyzer to every @Test |
Custom ITestListener (for reporting/screenshots)¶
public class TestListener implements ITestListener {
@Override
public void onTestStart(ITestResult result) {
ExtentReporter.startTest(result.getName());
}
@Override
public void onTestSuccess(ITestResult result) {
ExtentReporter.pass();
}
@Override
public void onTestFailure(ITestResult result) {
// capture screenshot, attach to report
WebDriver driver = (WebDriver) result.getTestContext().getAttribute("driver");
if (driver != null) {
String path = ScreenshotUtil.capture(driver, result.getName());
ExtentReporter.fail(result.getThrowable(), path);
}
}
}
testng.xml:
Or via annotation: @Listeners(TestListener.class) on the test class.
Retry — IRetryAnalyzer + IAnnotationTransformer¶
public class RetryAnalyzer implements IRetryAnalyzer {
private int count = 0;
private static final int MAX = 2;
@Override
public boolean retry(ITestResult result) {
if (count < MAX) { count++; return true; }
return false;
}
}
public class RetryListener implements IAnnotationTransformer {
@Override
public void transform(ITestAnnotation annotation, Class testClass,
Constructor testConstructor, Method testMethod) {
annotation.setRetryAnalyzer(RetryAnalyzer.class);
}
}
RetryAnalyzer to every @Test — devs don't have to opt in. Exactly the pattern your B2BProjectTest's Retry listener uses.
Follow-up they'll ask¶
- "Why use a Listener instead of putting cleanup in
@AfterMethod?" →@AfterMethodruns only after the test method. Listeners hook into pass/fail/skip explicitly — so you can do different things per outcome. Also, listeners don't pollute test code. - "How do you pass data from Listener to test?" → Through
ITestContext.setAttribute()(suite-wide) orITestResult.setAttribute()(test-wide).
Wrong answer that gets you rejected¶
"I take screenshots in
@AfterMethodif status is FAILURE." Why wrong: Works, but it puts framework-level concerns in test/base test code. Listener is the right level.
2.4 — dependsOnMethods / dependsOnGroups¶
What¶
Express test ordering and dependencies.
Code¶
@Test
public void login() { ... }
@Test(dependsOnMethods = "login")
public void addToCart() {
// skipped if login failed
}
@Test(groups = "checkout", dependsOnGroups = "cart")
public void payment() { ... }
Why interviewers ask¶
Many candidates abuse dependsOnMethods to enforce sequential execution. The right use case is functional dependency (login must succeed for cart to even be testable), not convenience ordering.
Follow-up they'll ask¶
- "What's wrong with chaining dependsOnMethods everywhere?" → It makes tests non-isolated. If login is flaky, your whole suite is flaky. Better: each test should set up its own state via API + fixtures.
- "What's
alwaysRun=trueon a @Test?" → The test runs even if its dependency failed. Useful for teardown-style verifications.
2.5 — Soft Assertions¶
Hard assertions stop at first failure¶
Assert.assertEquals(actualName, "Rohan"); // if fails, rest skipped
Assert.assertEquals(actualEmail, "r@x.com");
Soft assertions collect all, report at end¶
SoftAssert softAssert = new SoftAssert();
softAssert.assertEquals(actualName, "Rohan");
softAssert.assertEquals(actualEmail, "r@x.com");
softAssert.assertEquals(actualPhone, "12345");
softAssert.assertAll(); // CRITICAL — without this, failures silently passed
Follow-up they'll ask¶
- "When would you use Soft Assert?" → When validating a complex object with many fields — you want to see ALL mismatches in one run, not fix them one by one.
- "What if you forget
assertAll()?" → The test passes silently regardless of failures. Common pitfall.
2.6 — Suite XML — the parts that matter¶
<suite name="Suite" parallel="methods" thread-count="4" data-provider-thread-count="2">
<parameter name="env" value="stage"/>
<listeners>
<listener class-name="com.qa.listeners.TestListener"/>
</listeners>
<test name="API Tests">
<groups>
<run>
<include name="smoke"/>
<exclude name="slow"/>
</run>
</groups>
<classes>
<class name="com.qa.api.UsersTest"/>
<class name="com.qa.api.OrdersTest">
<methods>
<include name="testCreateOrder"/>
</methods>
</class>
</classes>
</test>
</suite>
Parallel modes¶
| Mode | Effect |
|---|---|
parallel="methods" |
Each @Test method in its own thread |
parallel="classes" |
Each test class in its own thread (your B2BProjectTest uses this) |
parallel="tests" |
Each <test> tag in its own thread |
parallel="instances" |
Each test class instance in its own thread |
thread-count caps concurrent threads. data-provider-thread-count caps parallel DataProvider rows.
PART 3: PLAYWRIGHT FIXTURES + TYPESCRIPT FOR TESTS¶
3.1 — test.extend (fixture creation)¶
What¶
Playwright's fixture system. The TS equivalent of pytest fixtures.
Basic custom fixture¶
import { test as base } from '@playwright/test';
type Fixtures = {
authenticatedPage: Page;
};
export const test = base.extend<Fixtures>({
authenticatedPage: async ({ page, request }, use) => {
// setup
const res = await request.post('/api/auth/login', {
data: { email: 'u@x.com', password: 'p' }
});
const { token } = await res.json();
await page.addInitScript((t) => {
localStorage.setItem('token', t);
}, token);
await page.goto('/dashboard');
// hand to test
await use(page);
// teardown (anything after use)
},
});
export { expect } from '@playwright/test';
Then in tests¶
import { test, expect } from '../fixtures';
test('dashboard loads', async ({ authenticatedPage }) => {
await expect(authenticatedPage.getByText('Welcome')).toBeVisible();
});
Why interviewers ask¶
Playwright fixtures replace beforeEach and are more powerful (lazy, composable, scoped). Knowing them = senior signal.
3.2 — Fixture scopes (test vs worker)¶
export const test = base.extend<{}, { dbConnection: any }>({
dbConnection: [async ({}, use) => {
const conn = await connectDb();
await use(conn);
await conn.close();
}, { scope: 'worker' }], // SCOPE OPTION HERE
});
| Scope | Created per | Use for |
|---|---|---|
test (default) |
each test | Stateful fixtures, fresh data |
worker |
each parallel worker process | DB connection, browser, expensive setup |
The TS generic test.extend<TestFixtures, WorkerFixtures> is how Playwright knows which are which.
3.3 — Auto fixtures and overriding¶
auto: true (no need to request)¶
export const test = base.extend({
metrics: [async ({}, use, testInfo) => {
const start = Date.now();
await use();
testInfo.attach('duration-ms', { body: String(Date.now() - start) });
}, { auto: true }],
});
Override built-in fixtures¶
export const test = base.extend({
page: async ({ page }, use) => {
// pre-instrument page
page.on('console', msg => console.log('Browser:', msg.text()));
await use(page);
},
});
3.4 — test.use (config per test/file)¶
test.use({ viewport: { width: 1920, height: 1080 } });
test.use({ storageState: 'auth-admin.json' }); // use admin auth for this file
test('admin can delete users', async ({ page }) => { ... });
Follow-up they'll ask¶
- "How do you run the same test file as different users?" → Two files, each with its own
test.use({ storageState: ... }). Or a custom fixture parameterized by role. - "How do you make a fixture run before EVERY test, even if not requested?" →
auto: true.
3.5 — TypeScript essentials for tests¶
Interfaces for API response shapes¶
interface User {
id: number;
email: string;
name: string;
role: 'admin' | 'viewer'; // literal union type
}
const res = await request.get('/api/users/1');
const user = await res.json() as User;
expect(user.role).toBe('admin');
Generic test helpers¶
async function getJson<T>(request: APIRequestContext, url: string): Promise<T> {
const res = await request.get(url);
expect(res.ok()).toBeTruthy();
return res.json() as T;
}
const user = await getJson<User>(request, '/api/users/1'); // typed!
Discriminated unions for response shapes¶
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string };
function handle(r: ApiResult<User>) {
if (r.ok) {
console.log(r.data.email); // TS knows data exists
} else {
console.log(r.error); // TS knows error exists
}
}
Wrong answer that gets you rejected¶
"I use
anyeverywhere in tests since they're just tests." Why wrong: Tests are code.anydefeats TypeScript's purpose and lets typos through. Useunknownif you really don't know the shape, then narrow with type guards.
PART 4: REST ASSURED DEEP DIVE¶
4.1 — Request and Response Specifications¶
Why¶
Avoid repeating headers, base URI, auth in every test. Build a spec once, reuse.
public class TestSpecs {
public static RequestSpecification authRequest(String token) {
return new RequestSpecBuilder()
.setBaseUri("https://api.example.com")
.setContentType(ContentType.JSON)
.addHeader("Authorization", "Bearer " + token)
.addFilter(new RequestLoggingFilter())
.addFilter(new ResponseLoggingFilter())
.build();
}
public static ResponseSpecification okJson() {
return new ResponseSpecBuilder()
.expectStatusCode(200)
.expectContentType(ContentType.JSON)
.expectResponseTime(Matchers.lessThan(2000L))
.build();
}
}
Use in tests¶
given()
.spec(TestSpecs.authRequest(token))
.pathParam("id", 42)
.when()
.get("/users/{id}")
.then()
.spec(TestSpecs.okJson())
.body("name", equalTo("Rohan"));
4.2 — POJOs for request/response (the right way)¶
Don't pass JSON strings around¶
Use POJOs (Jackson serializes automatically)¶
public class User {
private String name;
private String email;
// getters, setters, no-arg constructor
}
User u = new User();
u.setName("Rohan");
u.setEmail("r@x.com");
User created = given()
.contentType(JSON)
.body(u) // POJO → JSON automatically
.when()
.post("/users")
.then()
.statusCode(201)
.extract().as(User.class); // JSON → POJO
assertThat(created.getEmail()).isEqualTo("r@x.com");
Why interviewers ask¶
String concatenation for JSON is a junior smell. POJOs give compile-time field-name safety, IDE autocomplete, and refactor-safety.
4.3 — Authentication patterns¶
// Basic
.auth().basic("user", "pass")
.auth().preemptive().basic("user", "pass") // sends header immediately, no 401 challenge
// Bearer / OAuth2
.auth().oauth2(token)
.header("Authorization", "Bearer " + token) // equivalent
// 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");
// Form auth (with auto-CSRF detection)
.auth().form("user", "pass", new FormAuthConfig("/login", "username", "password"))
4.4 — Filters (the right way to log)¶
// Per-request
given().filter(new RequestLoggingFilter())
.filter(new ResponseLoggingFilter())
.when()...
// Global default
RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());
// Custom — extract every Authorization header for audit
RestAssured.filters((req, res, ctx) -> {
String auth = req.getHeaders().getValue("Authorization");
AuditLog.log(req.getMethod(), req.getURI(), auth);
return ctx.next(req, res);
});
4.5 — Extract and chain¶
// Path extraction
String token = given().body(login)
.post("/login").then()
.extract().path("data.access_token");
// Full response object
Response r = given().get("/users").then().extract().response();
int count = r.jsonPath().getInt("data.size()");
List<String> emails = r.jsonPath().getList("data.email");
List<User> users = r.jsonPath().getList("data", User.class);
// Headers
String reqId = r.getHeader("X-Request-Id");
4.6 — Schema validation¶
given().get("/users/1")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user.json"));
io.rest-assured:json-schema-validator.
The schema file (src/test/resources/schemas/user.json):
{
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": { "type": "integer" },
"email": { "type": "string", "format": "email" },
"name": { "type": "string", "minLength": 1 }
}
}
4.7 — Multipart and file upload¶
given()
.multiPart("file", new File("invoice.pdf"))
.multiPart("metadata", "{\"type\":\"invoice\"}", "application/json")
.when()
.post("/upload")
.then()
.statusCode(200);
4.8 — Common pitfalls¶
Static mutation (your B2BProjectTest has this!)¶
Fix:Assertion in API helper¶
public Response postOrder(JsonObject order) {
Response r = given().body(order).post("/orders");
Assert.assertEquals(r.getStatusCode(), 200); // ASSERT IN HELPER — bad
return r;
}
Wrong answer that gets you rejected¶
"I prefer building the request body as a JSON string." Why wrong: Brittle. POJO + Jackson is the senior pattern. Strings break when fields rename, no IDE help, no schema enforcement.
PART 5: SELENIUM PATTERNS (BEYOND THE BASICS)¶
5.1 — Explicit waits with custom conditions¶
Built-in¶
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit")));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));
wait.until(ExpectedConditions.textToBePresentInElement(elem, "Success"));
Custom condition (lambda)¶
wait.until(d -> d.findElements(By.cssSelector(".row")).size() == 10);
wait.until(d -> {
String txt = d.findElement(By.id("count")).getText();
return Integer.parseInt(txt) > 0;
});
Fluent wait (full control)¶
Wait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(30))
.pollingEvery(Duration.ofSeconds(1))
.ignoring(StaleElementReferenceException.class)
.ignoring(NoSuchElementException.class);
wait.until(d -> d.findElement(By.id("data")).isDisplayed());
5.2 — Actions class (mouse / keyboard)¶
Actions actions = new Actions(driver);
// Hover
actions.moveToElement(menu).perform();
// Drag and drop
actions.dragAndDrop(source, target).perform();
actions.clickAndHold(source).moveToElement(target).release().perform(); // manual
// Right click + double click
actions.contextClick(elem).perform();
actions.doubleClick(elem).perform();
// Key combinations
actions.keyDown(Keys.CONTROL).click(link).keyUp(Keys.CONTROL).perform(); // open in new tab
actions.sendKeys(Keys.chord(Keys.CONTROL, "a")).perform(); // select all
5.3 — JavaScriptExecutor¶
JavascriptExecutor js = (JavascriptExecutor) driver;
// Scroll
js.executeScript("window.scrollTo(0, document.body.scrollHeight);");
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", element);
// Force click (when element is overlapped)
js.executeScript("arguments[0].click();", element);
// Read localStorage / sessionStorage
String token = (String) js.executeScript("return localStorage.getItem('access_token');");
// Set value bypassing oninput handlers
js.executeScript("arguments[0].value = arguments[1];", inputField, "hello");
// Wait for jQuery / Angular / network idle
js.executeScript("return jQuery.active === 0;");
When to use¶
JS-executor is a last resort — bypasses Selenium's user-emulation. Use sparingly when: - Element is intercepted by overlay - Native scroll behavior is required - Reading browser storage
5.4 — Frames, windows, alerts¶
Frames¶
driver.switchTo().frame(0); // by index
driver.switchTo().frame("frameName"); // by name/id
driver.switchTo().frame(driver.findElement(By.tagName("iframe"))); // by element
driver.switchTo().parentFrame();
driver.switchTo().defaultContent(); // back to top
Multiple windows / tabs¶
String original = driver.getWindowHandle();
// click that opens new tab
for (String handle : driver.getWindowHandles()) {
if (!handle.equals(original)) {
driver.switchTo().window(handle);
break;
}
}
// do stuff in new tab
driver.close();
driver.switchTo().window(original);
Selenium 4 native API¶
Alerts¶
Alert alert = driver.switchTo().alert();
String text = alert.getText();
alert.accept(); // OK
alert.dismiss(); // Cancel
alert.sendKeys("input");
5.5 — Cookie management¶
// Add
driver.manage().addCookie(new Cookie("session_id", "abc123"));
// Read
Set<Cookie> cookies = driver.manage().getCookies();
Cookie c = driver.manage().getCookieNamed("session_id");
// Delete
driver.manage().deleteCookieNamed("session_id");
driver.manage().deleteAllCookies();
Reuse cookies to skip login (poor man's storageState)¶
// Save once
File f = new File("cookies.json");
new ObjectMapper().writeValue(f, driver.manage().getCookies());
// Load
List<Cookie> saved = new ObjectMapper().readValue(f, new TypeReference<>(){});
driver.get("https://example.com"); // need to navigate first
saved.forEach(c -> driver.manage().addCookie(c));
driver.navigate().refresh();
5.6 — Selenium 4 Chrome DevTools Protocol (CDP)¶
ChromeDriver chrome = (ChromeDriver) driver;
DevTools devTools = chrome.getDevTools();
devTools.createSession();
// Network throttling
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
devTools.send(Network.emulateNetworkConditions(false, 100, 10000, 5000, Optional.empty()));
// Capture network requests
devTools.addListener(Network.requestWillBeSent(), req ->
System.out.println(req.getRequest().getUrl()));
// Mock geolocation
devTools.send(Emulation.setGeolocationOverride(Optional.of(12.97), Optional.of(77.59), Optional.of(1)));
Why interviewers ask¶
CDP is what makes Selenium 4 feel modern. Bangalore product companies that use Selenium often want to know if you've leveraged it.
PART 6: HTTP CLIENTS BEYOND BASICS¶
6.1 — Python requests / httpx — sessions for connection reuse¶
Don't do this¶
Do this¶
with requests.Session() as s:
s.headers.update({"Authorization": f"Bearer {token}"})
for url in urls:
r = s.get(url) # reuses TCP, keeps cookies
Retry with urllib3¶
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE"],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
with requests.Session() as s:
s.mount("https://", adapter)
s.mount("http://", adapter)
r = s.get("https://api.example.com/health")
Async with httpx¶
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com")
# Parallel calls
responses = await asyncio.gather(
client.get("/users"),
client.get("/orders"),
client.get("/products"),
)
6.2 — Axios interceptors (your Morrie BaseApi pattern)¶
Auth header injection¶
import axios, { AxiosInstance } from 'axios';
export class BaseApi {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({ baseURL, timeout: 10_000 });
// Request interceptor: inject token
this.client.interceptors.request.use((config) => {
const token = process.env.ACCESS_TOKEN;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response interceptor: retry on 429
this.client.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'] ?? 1;
await new Promise(r => setTimeout(r, retryAfter * 1000));
return this.client.request(error.config);
}
throw error;
}
);
}
get<T>(url: string) { return this.client.get<T>(url); }
post<T>(url: string, data: any) { return this.client.post<T>(url, data); }
}
PART 7: MOCKING PATTERNS¶
7.1 — Mockito (Java)¶
Basic mock¶
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository repo;
@InjectMocks UserService service;
@Test
void getUserReturnsFromRepo() {
when(repo.findById(1)).thenReturn(new User("Rohan"));
User u = service.getUser(1);
assertEquals("Rohan", u.getName());
verify(repo).findById(1);
}
}
Argument captor¶
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(repo).save(captor.capture());
assertEquals("r@x.com", captor.getValue().getEmail());
Spy (partial mock — real method by default)¶
List<String> spy = spy(new ArrayList<>());
spy.add("hi");
verify(spy).add("hi");
assertEquals(1, spy.size()); // real method runs
7.2 — unittest.mock / pytest-mock (Python)¶
Basic patch¶
from unittest.mock import patch, MagicMock
def test_send_email(mocker): # pytest-mock provides 'mocker' fixture
mock_smtp = mocker.patch('myapp.smtplib.SMTP')
send_welcome_email("u@x.com")
mock_smtp.assert_called_once_with('smtp.gmail.com')
mock_smtp.return_value.sendmail.assert_called_once()
Patch a method on an object¶
def test_user_service(mocker):
mock_repo = mocker.patch('myapp.UserRepository.find_by_id')
mock_repo.return_value = {"id": 1, "name": "Rohan"}
service = UserService()
user = service.get_user(1)
assert user["name"] == "Rohan"
mock_repo.assert_called_once_with(1)
side_effect (for sequences / exceptions)¶
mock_repo.side_effect = [user1, user2, ConnectionError("DB down")]
service.get_user(1) # returns user1
service.get_user(2) # returns user2
service.get_user(3) # raises ConnectionError
7.3 — Playwright route mocking (your chat-eval framework uses this)¶
await page.route('**/api/users/*', async (route) => {
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 1, name: 'Mocked User' }),
});
} else {
await route.continue();
}
});
Why this matters¶
Lets you test UI behavior for error states (500, slow response, timeout) without coordinating with backend team.
7.4 — WireMock (standalone mock server)¶
@RegisterExtension
static WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig().port(8089))
.build();
@Test
void testWithMockedDependency() {
wm.stubFor(get(urlEqualTo("/external/users/1"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":1,\"name\":\"Rohan\"}")));
User u = myService.fetchUserFromExternalApi(1); // hits localhost:8089
assertEquals("Rohan", u.getName());
wm.verify(getRequestedFor(urlEqualTo("/external/users/1")));
}
When to use¶
Integration tests that exercise the real HTTP path but don't want to depend on an external service being up.
PART 8: PAGE OBJECT MODEL PATTERNS¶
8.1 — Basic POM¶
public class LoginPage {
private final WebDriver driver;
private final By emailInput = By.id("email");
private final By passwordInput = By.id("password");
private final By loginBtn = By.id("login");
public LoginPage(WebDriver driver) { this.driver = driver; }
public void login(String email, String password) {
driver.findElement(emailInput).sendKeys(email);
driver.findElement(passwordInput).sendKeys(password);
driver.findElement(loginBtn).click();
}
}
8.2 — Lazy locators (Playwright style — preferred)¶
export class LoginPage {
constructor(private page: Page) {}
// Return Locators, don't store them — re-query on use
private email = () => this.page.getByLabel('Email');
private password = () => this.page.getByLabel('Password');
private submit = () => this.page.getByRole('button', { name: 'Login' });
async login(email: string, password: string) {
await this.email().fill(email);
await this.password().fill(password);
await this.submit().click();
}
}
StaleElementReferenceException.
8.3 — Fluent POM (chain actions)¶
public class LoginPage {
public DashboardPage login(String email, String password) {
// ... fill + click
return new DashboardPage(driver); // return next page
}
}
// Test code reads like English
DashboardPage dashboard = new LoginPage(driver)
.login("user@x.com", "Test@123");
dashboard.assertWelcomeVisible();
8.4 — Component POM (composite)¶
For complex UIs with reusable components.
class Navbar {
constructor(private page: Page) {}
async clickProfile() { await this.page.getByTestId('nav-profile').click(); }
}
class Sidebar {
constructor(private page: Page) {}
async navigateTo(item: string) { await this.page.getByRole('link', { name: item }).click(); }
}
export class DashboardPage {
constructor(private page: Page) {}
navbar = new Navbar(this.page);
sidebar = new Sidebar(this.page);
async getMetrics() { /* page-specific */ }
}
// Test
await dashboard.navbar.clickProfile();
await dashboard.sidebar.navigateTo('Orders');
8.5 — Base page (shared infra)¶
public abstract class BasePage {
protected final WebDriver driver;
protected final WebDriverWait wait;
protected BasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
protected WebElement waitForClickable(By by) {
return wait.until(ExpectedConditions.elementToBeClickable(by));
}
protected void scrollIntoView(WebElement elem) {
((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", elem);
}
}
public class LoginPage extends BasePage {
public LoginPage(WebDriver d) { super(d); }
// Uses waitForClickable inherited
}
8.6 — POM anti-patterns¶
| Anti-pattern | Why bad |
|---|---|
| Assertions inside POM | POMs should expose state; tests should assert. Mixing them makes POMs single-purpose. |
| Mega-POMs (1 class per app) | Becomes 3000 lines, no one can navigate. Split per page. |
| Test data hard-coded in POM | POMs should take data as parameters. Hardcoding kills reusability. |
| Private locators exposed | Don't return WebElement to tests. Encapsulate. |
| Driver instantiated inside POM | POM should receive driver, not create it. |
PART 9: CI/CD SCRIPTING¶
9.1 — Jenkinsfile (declarative pipeline)¶
pipeline {
agent any
options {
timestamps()
ansiColor('xterm')
timeout(time: 60, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
}
parameters {
choice(name: 'SUITE', choices: ['smoke', 'regression', 'all'], description: 'Test suite')
string(name: 'BRANCH', defaultValue: 'main')
booleanParam(name: 'DRY_RUN', defaultValue: false)
}
environment {
NODE_ENV = 'test'
CI = 'true'
BASE_URL = credentials('stage-base-url') // Jenkins credential
}
stages {
stage('Checkout') {
steps {
checkout([$class: 'GitSCM', branches: [[name: "${params.BRANCH}"]]])
}
}
stage('Install') {
steps {
sh 'npm ci'
sh 'npx playwright install --with-deps chromium'
}
}
stage('Run Tests') {
parallel {
stage('API') {
when { expression { params.SUITE in ['smoke', 'all'] } }
steps {
sh 'npm run test:api'
}
}
stage('UI') {
when { expression { params.SUITE in ['regression', 'all'] } }
steps {
sh 'npm run test:ui'
}
}
}
}
}
post {
always {
archiveArtifacts artifacts: 'reports/**, test-results/**', allowEmptyArchive: true
junit testResults: 'reports/junit/*.xml', allowEmptyResults: true
publishHTML([
allowMissing: true,
keepAll: true,
reportDir: 'reports/html-report',
reportFiles: 'index.html',
reportName: 'Playwright Report'
])
}
failure {
emailext(
subject: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check console at ${env.BUILD_URL}",
to: 'qa-team@example.com'
)
}
}
}
Pipeline features to mention¶
- Parameters (choice, string, boolean) — devs self-serve different runs
- Parallel stages — API and UI run concurrently
- Credentials binding — secrets injected from Jenkins credential store
- Post-always — reports archived even if tests failed
- JUnit + HTML publishing — Jenkins UI shows results
9.2 — GitHub Actions workflow¶
name: Playwright Tests
on:
pull_request:
push:
branches: [main, develop]
schedule:
- cron: '0 2 * * *' # 2 AM IST nightly
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run tests
env:
BASE_URL: ${{ secrets.STAGE_BASE_URL }}
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
run: npx playwright test --shard=${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-shard-${{ strategy.job-index }}
path: playwright-report/
retention-days: 7
merge-reports:
needs: test
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: playwright-report-shard-*
path: all-reports
- run: npx playwright merge-reports --reporter=html ./all-reports
Concepts to mention¶
on: schedule— cron for nightly runsstrategy.matrix— sharding for parallel executionfail-fast: false— don't kill all shards if one failssecrets— env vars injected from GitHub repo secretsif: always()— upload report even on failureneeds— job dependencies
9.3 — GitLab CI (.gitlab-ci.yml)¶
stages:
- install
- test
- report
variables:
CI: "true"
npm_config_cache: "$CI_PROJECT_DIR/.npm"
cache:
paths:
- .npm/
- node_modules/
install:
stage: install
image: mcr.microsoft.com/playwright:v1.45.0-focal
script:
- npm ci
test:smoke:
stage: test
image: mcr.microsoft.com/playwright:v1.45.0-focal
script:
- npx playwright test --grep @smoke
artifacts:
when: always
paths:
- reports/
reports:
junit: reports/junit/*.xml
test:regression:
stage: test
parallel: 4
image: mcr.microsoft.com/playwright:v1.45.0-focal
script:
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
PART 10: TEST REPORTING¶
10.1 — Allure (works with Java, Python, JS)¶
Java setup (pom.xml)¶
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId>
<version>2.27.0</version>
</dependency>
Annotate tests¶
@Epic("Authentication")
@Feature("Login")
@Story("Valid user can log in")
@Severity(SeverityLevel.CRITICAL)
@Description("Verify a valid user logs in successfully")
@Test
public void testValidLogin() {
Allure.step("Open login page", () -> driver.get("/login"));
Allure.step("Enter credentials", () -> loginPage.login(EMAIL, PWD));
Allure.step("Verify dashboard", () -> assertTrue(dashboard.isVisible()));
}
// Attach screenshot on failure (via TestNG listener)
@Attachment(value = "Screenshot", type = "image/png")
public byte[] saveScreenshot(WebDriver driver) {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
}
Generate + view¶
Python (pytest)¶
import allure
@allure.feature("Login")
@allure.story("Valid login")
@allure.severity(allure.severity_level.CRITICAL)
def test_valid_login():
with allure.step("Navigate to login"):
page.goto("/login")
with allure.step("Enter credentials"):
page.fill("#email", "u@x.com")
page.fill("#password", "p")
10.2 — ExtentReports (Java — what B2BProjectTest uses)¶
Via TestNG listener¶
public class ExtentReporterNG implements ITestListener {
private static ExtentReports extent;
private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
@Override
public void onStart(ITestContext context) {
ExtentSparkReporter spark = new ExtentSparkReporter("target/extent-report.html");
spark.config().setTheme(Theme.DARK);
extent = new ExtentReports();
extent.attachReporter(spark);
}
@Override
public void onTestStart(ITestResult result) {
test.set(extent.createTest(result.getName()));
}
@Override
public void onTestSuccess(ITestResult result) {
test.get().log(Status.PASS, "Passed");
}
@Override
public void onTestFailure(ITestResult result) {
test.get().log(Status.FAIL, result.getThrowable());
// attach screenshot
}
@Override
public void onFinish(ITestContext context) {
extent.flush();
}
}
testng.xml:
10.3 — Playwright HTML reporter¶
// playwright.config.ts
reporter: [
['list'],
['html', { outputFolder: 'reports/html', open: 'never' }],
['json', { outputFile: 'reports/results.json' }],
['junit', { outputFile: 'reports/junit/results.xml' }],
['blob', { outputDir: 'reports/blob' }], // for sharding
],
10.4 — JUnit XML (universal CI format)¶
Almost every CI system (Jenkins, GitHub Actions, GitLab, CircleCI) understands JUnit XML for test result rendering.
PART 11: QUICK INTERVIEW DRILL (25 QUESTIONS)¶
Answer these from memory. Each is asked at Bangalore SDET interviews.
Pytest¶
- What's the difference between
functionandsessionscope? - Why use
yieldinstead ofreturnin a fixture? - What is
conftest.pyand how is it discovered? - How do you parametrize a test, and how do you give readable IDs?
- What's
autouse=Trueand when would you avoid it?
TestNG¶
- What's the order of
@BeforeSuite,@BeforeTest,@BeforeClass,@BeforeMethod? - How do you implement retry for failed tests in TestNG?
- Difference between
IRetryAnalyzerandIAnnotationTransformer? - Soft Assert vs Hard Assert — when to use which?
- What does
parallel="classes"do, and what's the gotcha with WebDriver?
Playwright¶
- What's
test.extendand how does it compare to pytest fixtures? - Difference between test-scope and worker-scope fixtures?
- How do you mock an API response in Playwright?
- What does
test.use({ storageState: 'auth.json' })do? - What's
auto: trueon a fixture?
REST Assured¶
- What's a RequestSpecification and why use it?
- POJO vs JSON string for request body — why prefer POJO?
- How do you extract a value from response for use in the next request?
- How do you validate JSON schema?
- Why is
RestAssured.baseURI = ...dangerous?
Selenium¶
- Implicit vs Explicit vs Fluent wait — when to use which?
- How does ThreadLocal driver enable parallel test execution?
- When would you use JavascriptExecutor?
- How do you switch between multiple tabs?
CI/CD¶
- Walk me through what your Jenkinsfile (or GitHub Actions workflow) does, stage by stage.
COMPANY-SPECIFIC NOTES (Bangalore)¶
Java + Selenium + TestNG roles¶
- Services companies (TCS, Infosys, Wipro, Cognizant) — expect deep TestNG (listeners, retry, DataProvider), Maven, Selenium Grid, BDD with Cucumber.
- Product companies that kept Java stacks (Walmart Labs, Flipkart, PayPal, Goldman Sachs, Cisco) — Java + Selenium + TestNG + REST Assured deep, plus Mockito.
Python SDET roles¶
- Razorpay, PhonePe, CRED, Postman, BrowserStack, Atlassian — Pytest fixtures, requests/httpx, sqlalchemy/psycopg2 for DB, pytest-xdist for parallel.
- AI / ML companies (Postman AI agents, AI startups) — Pytest + LLM evaluation patterns (like your chat-eval).
Playwright/TS SDET roles¶
- Modern startups (CRED, Postman, Hasura, Razorpay's newer teams) — Playwright fixtures, TypeScript, Axios interceptors, GitHub Actions.
FAANG Bangalore (Amazon, Microsoft, Google, Meta, Apple)¶
- Higher coding bar (Java/Python live coding to medium DSA level)
- Framework design round (45 min system design but for QA infrastructure)
- Behavioral round heavy (Amazon LP, Google Googleyness)
- Less emphasis on tool-specific syntax, more on principles
Banks + fintech (Goldman Sachs, JPMorgan, Citi, Razorpay)¶
- Strong on test data isolation, idempotency, audit trails
- Often ask about contract testing (Pact, Spring Cloud Contract)
- Performance testing baseline (JMeter, k6)
WHAT TO BRUSH UP BEFORE EACH ROUND TYPE¶
| Round | Files to revise |
|---|---|
| Phone screen | This file (skim PART 1, 2, 8); file 10 (45 questions) |
| Java technical | This file PART 2, 4, 5; file 13 Java section |
| Python technical | This file PART 1, 6; file 13 Python section |
| Playwright/TS technical | This file PART 3, 6, 7; file 13 JS section |
| Framework design | This file PART 8, 9, 10; file 14 (your real projects) |
| System design | File 11 Q9 (fintech framework design) |
| Behavioral | File 11 Q12, Q13, Q14 |
| AI testing | File 8; file 14 chat-eval section |
Good luck — and remember the honest framing: "I'd refactor this next" beats "my framework is perfect" every time in Bangalore senior interviews. Tech-debt awareness is the seniority signal.