Skip to content

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 isWhy interviewers askCode with explanationFollow-up they'll askWrong 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
Why 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
Rule: a test discovers all fixtures in its file + every 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.fixture and put cleanup in a try/finally inside the fixture." Why wrong: That works, but the idiomatic way is yield — cleaner, no nesting, plays nice with addfinalizer semantics. Interviewers using try/finally signals 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
- Each tuple becomes one test case - Test IDs appear in output: 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
    ...
Use when: you need fixture-managed setup per parameter (e.g., creating a real DB record per role).

Follow-up they'll ask

  • "How do you give test cases readable names?" → Use ids=:
    @pytest.mark.parametrize("email,expected", [...], ids=["valid", "no-at", "empty", "trailing-at"])
    
  • "Can you skip individual parametrized cases?" → Use pytest.param with marks:
    @pytest.mark.parametrize("x", [1, 2, pytest.param(3, marks=pytest.mark.skip(reason="bug"))])
    

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.mark.smoke
@pytest.mark.db
def test_login(): ...
Run only smoke: 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 -k do vs -m?"-k matches test name substring (e.g., -k login). -m matches marker.
  • "What's --strict-markers?" → Errors on unknown markers (typos). Add to pytest.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 via conftest.py propagation, 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 @DataProvider and <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);
        }
    }
}
Register in testng.xml:
<listeners>
    <listener class-name="com.qa.listeners.TestListener"/>
</listeners>
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);
    }
}
This auto-applies 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?"@AfterMethod runs 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) or ITestResult.setAttribute() (test-wide).

Wrong answer that gets you rejected

"I take screenshots in @AfterMethod if 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=true on 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 }],
});
Every test now has timing metrics attached, without requesting the fixture.

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);
  },
});
All your tests now log browser console output without changing test code.


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 any everywhere in tests since they're just tests." Why wrong: Tests are code. any defeats TypeScript's purpose and lets typos through. Use unknown if 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

// AVOID
String body = "{\"name\":\"Rohan\",\"email\":\"r@x.com\"}";
given().body(body)...

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"));
Add dependency: 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 }
  }
}
Catches contract drift before bugs hit production.


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!)

RestAssured.baseURI = "https://api.example.com";   // GLOBAL — breaks parallel tests
Fix:
given().baseUri("https://api.example.com").when()...   // instance-scoped

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;
}
Now this helper is useless for negative tests that expect 4xx. Move assertion to test, return response from helper.

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

driver.switchTo().newWindow(WindowType.TAB);
driver.switchTo().newWindow(WindowType.WINDOW);

Alerts

Alert alert = driver.switchTo().alert();
String text = alert.getText();
alert.accept();     // OK
alert.dismiss();    // Cancel
alert.sendKeys("input");

// 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

for url in urls:
    requests.get(url)   # new TCP connection per call — slow

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); }
}
This is the pattern your Morrie framework uses. Make sure you can explain it.


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();
  }
}
Why this beats Selenium's @FindBy: Locators are lazy and re-query DOM each use — no 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 runs
  • strategy.matrix — sharding for parallel execution
  • fail-fast: false — don't kill all shards if one fails
  • secrets — env vars injected from GitHub repo secrets
  • if: always() — upload report even on failure
  • needs — 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

mvn test
allure serve target/allure-results

Python (pytest)

pip install allure-pytest
pytest --alluredir=./allure-results
allure serve allure-results
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();
    }
}
Register in testng.xml:
<listeners>
    <listener class-name="com.qa.report.ExtentReporterNG"/>
</listeners>


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
],
npx playwright test
npx playwright show-report reports/html

10.4 — JUnit XML (universal CI format)

Almost every CI system (Jenkins, GitHub Actions, GitLab, CircleCI) understands JUnit XML for test result rendering.

// Playwright config
['junit', { outputFile: 'reports/junit/results.xml' }]
// TestNG generates this automatically in target/surefire-reports/
# Pytest
pytest --junitxml=reports/junit.xml


PART 11: QUICK INTERVIEW DRILL (25 QUESTIONS)

Answer these from memory. Each is asked at Bangalore SDET interviews.

Pytest

  1. What's the difference between function and session scope?
  2. Why use yield instead of return in a fixture?
  3. What is conftest.py and how is it discovered?
  4. How do you parametrize a test, and how do you give readable IDs?
  5. What's autouse=True and when would you avoid it?

TestNG

  1. What's the order of @BeforeSuite, @BeforeTest, @BeforeClass, @BeforeMethod?
  2. How do you implement retry for failed tests in TestNG?
  3. Difference between IRetryAnalyzer and IAnnotationTransformer?
  4. Soft Assert vs Hard Assert — when to use which?
  5. What does parallel="classes" do, and what's the gotcha with WebDriver?

Playwright

  1. What's test.extend and how does it compare to pytest fixtures?
  2. Difference between test-scope and worker-scope fixtures?
  3. How do you mock an API response in Playwright?
  4. What does test.use({ storageState: 'auth.json' }) do?
  5. What's auto: true on a fixture?

REST Assured

  1. What's a RequestSpecification and why use it?
  2. POJO vs JSON string for request body — why prefer POJO?
  3. How do you extract a value from response for use in the next request?
  4. How do you validate JSON schema?
  5. Why is RestAssured.baseURI = ... dangerous?

Selenium

  1. Implicit vs Explicit vs Fluent wait — when to use which?
  2. How does ThreadLocal driver enable parallel test execution?
  3. When would you use JavascriptExecutor?
  4. How do you switch between multiple tabs?

CI/CD

  1. 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.