Framework Design — Candescent SDET Interview Prep¶
Covers Round 1, Section 3 — "Functional & Automation: design a framework." This is the open-ended whiteboard round where the interviewer hands you a product (ride-sharing, e-commerce, streaming, banking) and expects you to produce a framework blueprint: layered architecture, major classes + hierarchy, components checklist, tool selection, test runners, and a sample test case. Talk through trade-offs out loud; they grade reasoning, not memorized lists.
0. How to attack ANY "design a framework" question (90-second opener)¶
When the interviewer names a product, say this structure back to them before diving in. It signals you have a reusable mental model:
- Clarify scope — platforms (web/iOS/Android/TV), API vs UI, environments, scale (hundreds vs thousands of tests), team size.
- Pick the architecture — layered, with strict dependency direction (tests depend on actions, actions depend on objects, objects depend on core — never the reverse).
- Name the layers + key classes.
- Select tools per layer and justify (Selenium vs Playwright, TestNG vs JUnit, Allure vs Extent).
- Walk one end-to-end sample test through every layer.
- Mention the cross-cutting concerns: parallelism + thread-safety, config/secrets, data management, reporting, CI.
The trick: the blueprint never changes. Only the page objects, API services, and business actions change per domain. Lead with that line — "My core framework is domain-agnostic; for any product I only swap the object and action layers." Interviewers love it.
1. The Universal Framework Blueprint (adapt to any domain in 5 minutes)¶
A layered framework with one-way dependencies. Each layer only knows about the layer directly below it. This is what makes it portable: the bottom four layers (Core, Config, Data, Reporting) are copy-paste reusable across projects; only the top three change.
+===========================================================================+
| LAYER 7 — CI / CD & REPORTING |
| Jenkins / GitHub Actions • Allure / Extent • Slack/email notify |
+===========================================================================+
^ triggers, publishes artifacts
+===========================================================================+
| LAYER 6 — TEST LAYER (the *what*) |
| @Test methods, TestNG suites/groups (smoke/regression/sanity) |
| Pure intent: "user books a ride and sees the driver move" |
+---------------------------------------------------------------------------+
| calls business actions only (no locators, no waits here)
v
+===========================================================================+
| LAYER 5 — BUSINESS ACTION / WORKFLOW LAYER (the *how, in domain terms*) |
| RideService.bookRide(), CheckoutFlow.purchase(), BankFlow.transfer() |
| Composes multiple page objects / API calls into one business operation |
+---------------------------------------------------------------------------+
| drives
v
+===========================================================================+
| LAYER 4 — OBJECT LAYER (Page Objects + API Service Objects) |
| LoginPage, CartPage, PlayerPage | AuthApi, AccountsApi, TransferApi |
| Knows locators / endpoints. NO test assertions about business outcomes. |
+---------------------------------------------------------------------------+
| uses
v
+===========================================================================+
| LAYER 3 — CORE / UTILITIES (domain-agnostic engine) |
| DriverFactory (ThreadLocal) WaitUtils ApiClient ElementActions |
| RetryAnalyzer ScreenshotUtil JsonUtil DbUtil AssertionsWrapper |
+---------------------------------------------------------------------------+
| reads
v
+===========================================================================+
| LAYER 2 — CONFIG / ENVIRONMENT | LAYER 1 — TEST DATA |
| ConfigReader, env profiles (qa/dev/ | TestDataProvider, Excel/JSON/ |
| preprod/prod), SecretsManager, URLs, | DB/Faker, DataModels (POJOs), |
| timeouts, capabilities | builders |
+===========================================================================+
Dependency rule (say this): "Compile-time dependencies point downward only. A page object must never import a test; core utils must never import a page object. This keeps the core reusable and prevents circular coupling."
What you swap per domain vs what you keep:
| Layer | Reusable across all domains? |
|---|---|
| 7 CI/Reporting | Yes (config tweaks only) |
| 6 Tests | No — domain-specific |
| 5 Business Actions | No — domain-specific |
| 4 Objects (Pages/APIs) | No — domain-specific |
| 3 Core/Utils | Yes — 100% portable |
| 2 Config/Env | Yes (values differ, code same) |
| 1 Data | Mostly (models differ, plumbing same) |
2. Major Classes + Hierarchy (name these explicitly)¶
BaseTest (TestNG @BeforeMethod/@AfterMethod lifecycle, driver setup/teardown)
├── WebBaseTest (web-specific: launch browser via DriverFactory)
├── ApiBaseTest (RestAssured spec setup, base URI, auth token)
└── MobileBaseTest (Appium driver, capabilities)
BasePage (common element interactions, delegates to WaitUtils + ElementActions)
├── LoginPage
├── AccountsPage / DashboardPage
└── ...one subclass per screen
DriverFactory (creates WebDriver/AppiumDriver per browser/platform)
DriverManager (ThreadLocal<WebDriver> holder — get(), set(), remove())
ConfigReader (loads env config: properties/YAML, system props override)
SecretsManager (pulls secrets from env vars / vault, never from git)
WaitUtils (explicit waits: visible, clickable, presence, custom conditions)
ElementActions (safe click, type, select dropdown, JS fallback, scroll)
ScreenshotUtil (capture on failure, embed in report)
RetryAnalyzer implements IRetryAnalyzer (re-run flaky tests N times)
AssertionsWrapper / SoftAssertManager
ApiClient (thin RestAssured wrapper: given/when/then, logging, auth filter)
└── *Api / *Service classes (AuthApi, AccountsApi, TransferApi — one per resource)
TestDataProvider (@DataProvider methods; reads Excel/JSON/DB)
DataModels (POJOs) (Account, Ride, Order, Movie — with builders)
ReportManager (Allure/Extent wiring)
Listeners:
TestListener implements ITestListener (log start/pass/fail, screenshot on fail)
SuiteListener implements ISuiteListener
RetryListener implements IAnnotationTransformer (auto-attach RetryAnalyzer to every @Test)
2.1 Sample DriverFactory + DriverManager (thread-safe)¶
public final class DriverManager {
private static final ThreadLocal<WebDriver> TL_DRIVER = new ThreadLocal<>();
private DriverManager() {}
public static WebDriver getDriver() {
return TL_DRIVER.get();
}
static void setDriver(WebDriver driver) {
TL_DRIVER.set(driver);
}
public static void quitDriver() {
WebDriver driver = TL_DRIVER.get();
if (driver != null) {
driver.quit();
TL_DRIVER.remove(); // CRITICAL: prevents memory leak + cross-test bleed in parallel runs
}
}
}
public final class DriverFactory {
public static WebDriver createDriver(String browser, boolean grid, String gridUrl) {
WebDriver driver;
switch (browser.toLowerCase()) {
case "chrome" -> {
ChromeOptions opts = new ChromeOptions();
opts.addArguments("--disable-notifications", "--start-maximized");
driver = grid
? new RemoteWebDriver(toUrl(gridUrl), opts)
: new ChromeDriver(opts);
}
case "firefox" -> {
FirefoxOptions opts = new FirefoxOptions();
driver = grid ? new RemoteWebDriver(toUrl(gridUrl), opts) : new FirefoxDriver(opts);
}
case "edge" -> driver = new EdgeDriver(new EdgeOptions());
default -> throw new IllegalArgumentException("Unsupported browser: " + browser);
}
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
DriverManager.setDriver(driver);
return driver;
}
private static URL toUrl(String url) {
try { return new URL(url); }
catch (MalformedURLException e) { throw new RuntimeException("Bad grid URL: " + url, e); }
}
}
2.2 Sample BasePage¶
public abstract class BasePage {
protected final WebDriver driver;
protected final WebDriverWait wait;
protected BasePage() {
this.driver = DriverManager.getDriver(); // pulled from ThreadLocal — parallel-safe
this.wait = new WebDriverWait(driver, Duration.ofSeconds(
ConfigReader.getInt("explicit.wait.seconds", 15)));
}
protected WebElement waitVisible(By locator) {
return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
protected WebElement waitClickable(By locator) {
return wait.until(ExpectedConditions.elementToBeClickable(locator));
}
protected void click(By locator) {
waitClickable(locator).click();
}
protected void type(By locator, String text) {
WebElement el = waitVisible(locator);
el.clear();
el.sendKeys(text);
}
protected String text(By locator) {
return waitVisible(locator).getText();
}
protected boolean isDisplayed(By locator) {
try { return waitVisible(locator).isDisplayed(); }
catch (TimeoutException e) { return false; }
}
}
2.3 Sample BaseTest + listener wiring¶
@Listeners({TestListener.class, RetryListener.class})
public abstract class WebBaseTest {
@Parameters({"browser"})
@BeforeMethod(alwaysRun = true)
public void setUp(@Optional("chrome") String browser) {
boolean grid = ConfigReader.getBool("selenium.grid", false);
WebDriver driver = DriverFactory.createDriver(browser, grid, ConfigReader.get("grid.url"));
driver.get(ConfigReader.get("base.url"));
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
DriverManager.quitDriver();
}
}
public class RetryAnalyzer implements IRetryAnalyzer {
private int count = 0;
private static final int MAX = ConfigReader.getInt("retry.count", 1);
@Override
public boolean retry(ITestResult result) {
if (count < MAX) { count++; return true; }
return false;
}
}
2.4 Sample ApiClient + service class (RestAssured)¶
public final class ApiClient {
public static RequestSpecification base() {
return RestAssured.given()
.baseUri(ConfigReader.get("api.base.uri"))
.contentType(ContentType.JSON)
.filter(new AllureRestAssured()) // auto-attaches req/resp to Allure
.header("Authorization", "Bearer " + SecretsManager.token());
}
}
public class TransferApi {
public Response transfer(TransferRequest req) {
return ApiClient.base()
.header("Idempotency-Key", req.getIdempotencyKey()) // banking: safe retries
.body(req)
.when().post("/v1/transfers")
.then().extract().response();
}
public AccountBalance getBalance(String accountId) {
return ApiClient.base()
.when().get("/v1/accounts/{id}/balance", accountId)
.then().statusCode(200)
.extract().as(AccountBalance.class);
}
}
3. Framework Components Checklist¶
| Concern | Choice | Why / Notes |
|---|---|---|
| Language | Java (primary), TypeScript (Playwright) | Match the team's stack; Candescent is Java-heavy BFSI |
| Test runner | TestNG | Groups, parallel, data providers, listeners, retry, dependsOn |
| Build / dependency | Maven (POM, surefire/failsafe) | mvn test -Pqa -Dbrowser=chrome; profiles per env |
| UI automation | Selenium 4 (Grid-ready) + Playwright (TS) for new/SPA work | Selenium for legacy + broad browser matrix; Playwright for speed + auto-wait |
| API automation | RestAssured | Fluent given/when/then, schema validation, serialization |
| Mobile | Appium (iOS/Android), one driver abstraction | Reuse page-object pattern via screens |
| Cross-browser | Selenium Grid (Docker) / BrowserStack / Playwright projects | Grid for self-hosted, BrowserStack for real devices + matrix |
| Multi-environment | config-{env}.properties or Maven profiles |
-Penv=preprod; system props override file values |
| Data-driven | Excel (Apache POI), JSON (Jackson), DB (JDBC), Faker for synthetic | @DataProvider; POJOs + builders |
| Reporting | Allure (primary) + Extent (stakeholder-friendly) | Allure for trends/history; Extent for a single shareable HTML |
| CI/CD | Jenkins (pipeline) or GitHub Actions | Nightly regression + per-PR smoke; publish Allure |
| Parallel execution | TestNG parallel="methods" thread-count="N" + ThreadLocal driver |
Cut a 4-hr suite to ~30 min |
| Secrets | Env vars / Jenkins credentials / HashiCorp Vault | NEVER in git; SecretsManager indirection |
| Logging | Log4j2 / SLF4J | Structured logs per thread, correlation IDs |
| Assertions | TestNG asserts + soft asserts; AssertJ for fluency | Soft asserts to collect multiple UI checks |
| Version control | Git, trunk-based or GitFlow | PR gates run smoke suite |
Example pom.xml profile + surefire snippet¶
<profiles>
<profile>
<id>qa</id>
<properties><env>qa</env></properties>
<activation><activeByDefault>true</activeByDefault></activation>
</profile>
<profile><id>preprod</id><properties><env>preprod</env></properties></profile>
<profile><id>prod</id><properties><env>prod</env></properties></profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<suiteXmlFiles><suiteXmlFile>suites/${suite:-regression}.xml</suiteXmlFile></suiteXmlFiles>
<systemPropertyVariables><env>${env}</env></systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
Example TestNG suite (parallel)¶
<suite name="Regression" parallel="methods" thread-count="5">
<listeners>
<listener class-name="com.framework.listeners.TestListener"/>
</listeners>
<test name="WebRegression">
<parameter name="browser" value="chrome"/>
<classes>
<class name="com.tests.CheckoutTest"/>
<class name="com.tests.LoginTest"/>
</classes>
</test>
</suite>
4. Cross-Platform Strategy¶
One framework, multiple drivers behind a common abstraction. The business action layer is platform-agnostic — it calls loginPage.login(user, pass); whether that resolves to a Selenium, Appium, or Playwright object is decided by the DriverFactory from config.
| Platform | Tool | Driver / Mechanism |
|---|---|---|
| Web (desktop browsers) | Selenium 4 / Playwright | ChromeDriver, GeckoDriver, RemoteWebDriver via Grid |
| iOS native/hybrid | Appium + XCUITest | XCUITestOptions, real device or simulator |
| Android native/hybrid | Appium + UiAutomator2 | UiAutomator2Options, real device or emulator |
| Mobile web | Selenium (mobile emulation) / Appium browser | Chrome mobile emulation or device browser |
| Smart-TV / set-top / OTT | AWS Device Farm / BrowserStack / Sauce Labs device cloud | Vendor-driven; Appium-for-TV (tvOS/Android TV) where supported |
| Real-device matrix at scale | BrowserStack / Sauce Labs | Cloud capabilities injected into RemoteWebDriver/Appium |
| Backend / integration | RestAssured (Java) / API request fixtures (Playwright) | Direct API for setup, teardown, and verification |
Key design move (mention this): Use API calls to set up state fast and assert backend truth, then drive UI only for what genuinely needs UI. "I book a ride via API to create state, then verify driver-tracking on the UI" — faster and less flaky than doing everything through the screen.
Platform abstraction sketch¶
public interface AppDriver { // common contract
void launch();
void quit();
}
public class WebDriverAdapter implements AppDriver { /* Selenium */ }
public class MobileDriverAdapter implements AppDriver { /* Appium */ }
// DriverFactory picks the adapter from config: platform=web|ios|android|tv
5. Worked Answers — The Three JD Sample Questions¶
For each: re-skinned blueprint, key classes, components, tools/runners, a banking parallel, and a sample test case.
Q1 — Ride-Sharing App (Uber-like)¶
Scope: user registration, ride booking, driver tracking, fare calculation. Platforms: iOS + Android + backend APIs.
Layered layout (only objects/actions change from the universal blueprint):
Tests: RegistrationTest, BookRideTest, FareCalcTest, DriverTrackingTest
Actions: RideFlow (register -> book -> assignDriver -> track -> pay),
FareService.calculate()
Objects:
Mobile screens: SignupScreen, HomeMapScreen, BookingScreen, TrackingScreen, ReceiptScreen
API services: AuthApi, RiderApi, DriverApi, BookingApi, FareApi, LocationApi
Core: AppiumDriverFactory (ThreadLocal), WaitUtils, GeoUtils, ApiClient
Config/Data: env profiles, TestDataProvider (riders, routes), Faker for users
Major classes + hierarchy: MobileBaseTest -> RegistrationTest; BaseScreen -> BookingScreen/TrackingScreen; ApiClient -> BookingApi/FareApi/LocationApi; AppiumDriverFactory + DriverManager(ThreadLocal).
Components: TestNG runner; Maven; Appium for iOS/Android (XCUITest + UiAutomator2); BrowserStack/Sauce real-device cloud; RestAssured for backend; Allure reporting; Jenkins nightly; parallel across device pool.
Tools/runners: Appium + TestNG + RestAssured; device cloud for the matrix.
Hard parts to call out:
- Driver tracking = real-time location stream. Don't try to assert pixel-perfect map movement. Instead: drive a mock driver location via the LocationApi, then assert the UI marker/ETA updates (poll with explicit wait for the displayed ETA/coordinates to change).
- Fare calculation = pure backend logic; test it primarily at the API layer with table-driven data (distance, surge, time-of-day) for precise math; a couple of UI smoke checks suffice.
Banking parallel: "Driver tracking via a real-time location stream" maps directly to real-time account balance updates after a transfer. Same technique: trigger a backend event (location update / posted transaction), then poll the UI until the displayed value reflects it. Fare calculation = interest/fee calculation — precise decimal math, best verified at the API layer with data-driven cases.
Sample test case — book a ride and track driver:
public class BookRideTest extends MobileBaseTest {
@Test(groups = "smoke")
public void bookRideAndTrackDriver() {
// 1. Arrange state fast via API
String riderToken = new AuthApi().loginRider("rider_qa@test.com", "Pass@123");
BookingResponse booking = new BookingApi()
.requestRide(riderToken, RideRequest.builder()
.pickup(new LatLng(40.7128, -74.0060))
.drop(new LatLng(40.7580, -73.9855)).build());
Assert.assertEquals(booking.getStatus(), "DRIVER_ASSIGNED");
// 2. Simulate driver moving (mock location stream)
new DriverApi().pushLocation(booking.getDriverId(), new LatLng(40.7300, -73.9990));
// 3. Verify on the UI (Appium)
TrackingScreen tracking = new HomeMapScreen().openActiveRide();
Assert.assertTrue(tracking.isDriverMarkerVisible(), "Driver marker should appear");
String etaBefore = tracking.getEta();
new DriverApi().pushLocation(booking.getDriverId(), new LatLng(40.7450, -73.9900));
tracking.waitForEtaToChange(etaBefore); // explicit wait, no Thread.sleep
Assert.assertTrue(tracking.getEtaMinutes() < parse(etaBefore), "ETA should decrease");
}
}
Q2 — E-Commerce App (Amazon-like)¶
Scope: login/logout/accounts, cart/products/orders, checkout/payments, profile/reward points. Multi-browser, multi-environment (QA/dev/pre-prod). Deliverables explicitly requested: framework layout, major classes + hierarchy, major components, approach/tools/runners, sample test.
Framework layout:
src/test/java
tests/ LoginTest, SearchTest, CartTest, CheckoutTest, RewardPointsTest
flows/ CheckoutFlow, AuthFlow (business actions)
pages/ LoginPage, SearchResultsPage, ProductPage, CartPage, CheckoutPage, OrderConfirmPage
api/ AuthApi, CatalogApi, CartApi, OrderApi, PaymentApi
core/ DriverFactory, DriverManager, WaitUtils, ElementActions, RetryAnalyzer
config/ ConfigReader, SecretsManager
data/ TestDataProvider, models (Product, Cart, Order), builders
listeners/ TestListener, RetryListener
src/test/resources
config/ config-dev.properties, config-qa.properties, config-preprod.properties
suites/ smoke.xml, regression.xml
testdata/ products.json, users.xlsx
Major classes + hierarchy:
WebBaseTest -> CheckoutTest
BasePage -> LoginPage / ProductPage / CartPage / CheckoutPage
ApiClient -> CatalogApi / CartApi / OrderApi / PaymentApi
DriverFactory + DriverManager(ThreadLocal<WebDriver>)
ConfigReader, WaitUtils, TestDataProvider, ReportManager
TestListener implements ITestListener
Major components: TestNG runner with groups (smoke/regression); Maven with -Penv=qa|dev|preprod profiles; cross-browser via Selenium Grid (Docker) or BrowserStack (-Dbrowser=chrome|firefox|edge); data-driven via JSON/Excel; Allure + Extent reporting; Jenkins/GitHub Actions; parallel by methods with ThreadLocal; secrets via env vars (payment sandbox keys).
Approach/tools/runners: Java + TestNG + Selenium 4 + RestAssured; dummy/sandbox payment gateway (Stripe test card 4242 4242 4242 4242 or app's mock) so no real money moves; API to seed catalog/user and to assert order persistence after UI checkout.
Multi-environment handling:
public final class ConfigReader {
private static final Properties P = new Properties();
static {
String env = System.getProperty("env", "qa"); // -Denv=preprod
try (InputStream in = ConfigReader.class.getResourceAsStream("/config/config-" + env + ".properties")) {
P.load(in);
} catch (IOException e) { throw new RuntimeException("Missing config for env=" + env, e); }
}
public static String get(String key) {
return System.getProperty(key, P.getProperty(key)); // system prop overrides file
}
public static int getInt(String k, int def) { String v = get(k); return v==null?def:Integer.parseInt(v); }
public static boolean getBool(String k, boolean def){ String v=get(k); return v==null?def:Boolean.parseBoolean(v); }
}
Banking parallel: cart/checkout/payment is structurally identical to add payee -> initiate transfer -> confirm with dummy/sandbox rails. Reward points = loyalty/cashback or interest accrual. Multi-environment QA/dev/preprod is exactly how banks gate releases (and prod is read-only/synthetic-account-only for automation).
Sample test — search -> add to cart -> checkout with dummy payment:
public class CheckoutTest extends WebBaseTest {
@Test(groups = {"smoke", "checkout"})
public void searchAddToCartCheckout() {
// Seed a known logged-in session via API token for speed
String token = new AuthApi().login("shopper_qa@test.com", SecretsManager.get("shopper.pwd"));
// UI flow
new LoginPage().loginWithToken(token); // or full UI login
SearchResultsPage results = new HomePage().search("wireless mouse");
ProductPage product = results.openFirstResult();
Assert.assertTrue(product.isAddToCartEnabled());
CartPage cart = product.addToCart().goToCart();
Assert.assertEquals(cart.getItemCount(), 1);
CheckoutPage checkout = cart.proceedToCheckout();
OrderConfirmPage confirm = checkout
.useSavedAddress()
.payWithCard("4242424242424242", "12/29", "123"); // sandbox card — no real charge
String orderId = confirm.getOrderId();
Assert.assertTrue(confirm.isSuccessMessageShown(), "Order success banner expected");
// Backend assertion — UI lied? API is the source of truth
Order order = new OrderApi().getOrder(token, orderId);
Assert.assertEquals(order.getStatus(), "CONFIRMED");
Assert.assertEquals(order.getItems().size(), 1);
}
}
Q3 — Video Streaming App (Netflix-like)¶
Scope: login, browsing, playback, subscription. Platforms: smartphones, tablets, smart-TVs, web, mobile. Strategy for different video resolutions / streaming qualities.
Layered layout:
Tests: LoginTest, BrowseTest, PlaybackTest, SubscriptionTest, ResolutionMatrixTest
Actions: PlaybackFlow (login -> search -> play -> assert playing),
SubscriptionFlow
Objects:
Web pages: LoginPage, BrowsePage, TitleDetailPage, PlayerPage
Mobile/TV: LoginScreen, BrowseScreen, PlayerScreen (Appium / device-cloud)
API services: AuthApi, CatalogApi, PlaybackApi (manifest/CDN), SubscriptionApi
Core: DriverFactory(multi-platform), WaitUtils, MediaUtils, ApiClient
Major classes: BaseTest -> PlaybackTest; BasePage/BaseScreen -> PlayerPage/PlayerScreen; ApiClient -> PlaybackApi/SubscriptionApi; multi-platform DriverFactory.
Components: TestNG; Maven; device farm (BrowserStack/Sauce/AWS Device Farm) for the smartphone/tablet/smart-TV matrix; Appium for native + TV apps; Selenium/Playwright for web; RestAssured for the manifest/entitlement APIs; Allure reporting; Jenkins matrix builds.
Tools/runners: TestNG + Appium (mobile/TV) + Selenium/Playwright (web) + RestAssured. Run the platform matrix as parallel TestNG <test> blocks or CI matrix jobs.
Resolution / streaming-quality strategy (the differentiator they're probing):
- Don't decode pixels in UI tests. Verify quality the way the app negotiates it: the HLS/DASH manifest and the player's reported state.
- API/manifest level: fetch the manifest via PlaybackApi, assert it advertises the expected ABR variants (240p/480p/720p/1080p/4K bitrates).
- Player-state level: the player exposes current resolution/bitrate (via JS API on web, accessibility/automation IDs on apps). Assert the player reports "PLAYING" and the selected quality.
- Network shaping: use network throttling (Chrome DevTools Protocol / device-cloud network profiles) to force low bandwidth, then assert ABR down-switches to a lower bitrate and playback continues without stalling beyond threshold.
- Buffering/QoE metrics: assert startup time < threshold and rebuffer count via the player's telemetry API rather than eyeballing.
@Test(dataProvider = "qualities")
public void abrSelectsExpectedVariant(String label, int kbps, String expectedRes) {
NetworkUtils.throttle(kbps); // CDP / device-cloud profile
PlayerPage player = playbackFlow.play("Stranger Things");
player.waitForState("PLAYING");
Assert.assertEquals(player.getCurrentResolution(), expectedRes,
"ABR should pick " + expectedRes + " at " + kbps + "kbps");
}
Banking parallel: the multi-device/multi-platform matrix (phone/tablet/TV/web) is the same problem as a bank shipping web + iOS + Android + tablet banking apps from one codebase — one framework, platform abstraction, device cloud. "Adaptive quality" maps to graceful degradation / responsive layouts across screen sizes and to feature-flag/entitlement checks (premium tier == premium banking products).
Sample test — login -> search movie -> start playback:
public class PlaybackTest extends BaseTest { // platform from config: web|android|ios|tv
@Test(groups = "smoke")
public void loginSearchPlay() {
LoginPage login = new LoginPage();
BrowsePage browse = login.login("viewer_qa@test.com", SecretsManager.get("viewer.pwd"));
TitleDetailPage title = browse.search("Inception").openFirstResult();
Assert.assertTrue(title.isPlayButtonVisible());
PlayerPage player = title.play();
player.waitForState("PLAYING"); // polls player JS/automation API
Assert.assertTrue(player.isPlaying(), "Video should be in PLAYING state");
Assert.assertTrue(player.getCurrentTimeSeconds() > 2, // playhead actually advanced
"Playhead should advance, proving real playback");
}
}
6. FOURTH Example — Candescent-Style Digital Banking Portal (the domain-fit differentiator)¶
Scope: login + MFA, view accounts/balances, fund transfer, bill pay, transaction history. Web + mobile. This is where you show you understand BFSI, not just generic automation.
Layered layout (same blueprint, banking objects):
Tests: LoginMfaTest, AccountsTest, FundTransferTest, BillPayTest, TxnHistoryTest
Actions: AuthFlow (login -> MFA -> dashboard),
TransferFlow (selectAccounts -> enterAmount -> confirm -> verifyBalances),
BillPayFlow
Objects:
Pages: LoginPage, MfaPage, DashboardPage, AccountsPage, TransferPage,
TransferConfirmPage, BillPayPage, TransactionHistoryPage
API: AuthApi, OtpApi, AccountsApi, TransferApi, BillPayApi, TransactionApi
Core: DriverFactory(ThreadLocal), WaitUtils, MoneyUtils(BigDecimal), ApiClient,
AuditUtils, SessionUtils
Config: env profiles (dev/qa/preprod/prod-readonly), SecretsManager (Vault)
Data: synthetic test accounts only, TestDataProvider, Account/Transfer POJOs
Banking-specific concerns (call every one of these out — it's the differentiator):
| Concern | How the framework handles it |
|---|---|
| Money / decimal precision | Never use double/float for money. Use BigDecimal with explicit scale + RoundingMode. MoneyUtils.equal(a,b) compares with compareTo, not equals (so 10.0 == 10.00). Assert balances to the cent. |
| Idempotent transfers | Every transfer carries an Idempotency-Key; test that retrying the same key does NOT double-debit. This is a must-mention in BFSI. |
| Real-time balance updates | After transfer, poll source/destination balances via AccountsApi until they reflect the debit/credit (explicit wait, not sleep). Assert sourceBefore - amount == sourceAfter and destBefore + amount == destAfter. |
| Audit trail | After any money movement, assert an audit/ledger entry exists (TransactionApi.getHistory() contains the txn with correct who/when/amount/status). |
| PCI-DSS / PII | No real card/account numbers in code or logs; mask in reports (****1234); secrets from Vault/env; logs scrubbed of PII; test data is synthetic. |
| Session timeout / security | Test idle-timeout logout, re-auth on sensitive actions, no back-button access after logout, MFA enforced. |
| MFA / OTP | Use a test OTP service / fixed sandbox OTP, or fetch OTP via a test API rather than SMS. Never hardcode prod OTP logic. |
| Negative / limits | Insufficient funds, over daily-limit, invalid payee, expired session — all explicit test cases. BFSI cares more about negative paths than happy path. |
public final class MoneyUtils {
public static BigDecimal money(String v) { return new BigDecimal(v).setScale(2, RoundingMode.HALF_EVEN); }
public static boolean equal(BigDecimal a, BigDecimal b) { return a.compareTo(b) == 0; } // not .equals!
}
Sample test — fund transfer with balance + idempotency + audit assertions:
public class FundTransferTest extends WebBaseTest {
@Test(groups = {"smoke", "money"})
public void transferDebitsAndCreditsCorrectly() {
String token = new AuthApi().login("acct_qa@test.com", SecretsManager.get("acct.pwd"));
AccountsApi accounts = new AccountsApi();
BigDecimal srcBefore = accounts.balance(token, "CHK-001");
BigDecimal dstBefore = accounts.balance(token, "SAV-001");
BigDecimal amount = MoneyUtils.money("250.00");
// UI: login -> MFA -> transfer
DashboardPage dash = new LoginPage().login("acct_qa@test.com", SecretsManager.get("acct.pwd"))
.enterOtp(new OtpApi().fetchTestOtp("acct_qa@test.com"));
TransferConfirmPage confirm = dash.openTransfer()
.from("CHK-001").to("SAV-001").amount("250.00").review();
Assert.assertEquals(MoneyUtils.money(confirm.getDisplayedAmount()), amount);
String reference = confirm.submit().getConfirmationReference();
// Real-time balance check (poll until settled)
BigDecimal srcAfter = accounts.waitForBalanceChange(token, "CHK-001", srcBefore);
BigDecimal dstAfter = accounts.balance(token, "SAV-001");
Assert.assertTrue(MoneyUtils.equal(srcAfter, srcBefore.subtract(amount)), "Source debited by amount");
Assert.assertTrue(MoneyUtils.equal(dstAfter, dstBefore.add(amount)), "Dest credited by amount");
// Audit trail
Transaction txn = new TransactionApi().findByReference(token, reference);
Assert.assertEquals(txn.getStatus(), "COMPLETED");
Assert.assertEquals(MoneyUtils.money(txn.getAmount()), amount);
// Idempotency — replaying same key must NOT double-debit
Response replay = new TransferApi().transfer(TransferRequest.builder()
.idempotencyKey(confirm.getIdempotencyKey())
.from("CHK-001").to("SAV-001").amount("250.00").build());
BigDecimal srcAfterReplay = accounts.balance(token, "CHK-001");
Assert.assertTrue(MoneyUtils.equal(srcAfterReplay, srcAfter), "Replay must not debit again");
}
}
7. Likely Follow-Up Q&A (rapid-fire)¶
Q1. How do you handle flaky tests?
Root-cause first, don't just retry. Common causes: implicit+explicit wait mixing, Thread.sleep, non-deterministic test data, shared state in parallel, animations. Fixes: explicit waits / Playwright auto-wait, stable locators (data-testid, not XPath-by-position), independent self-seeding tests, RetryAnalyzer (cap at 1–2 retries) as a safety net, quarantine flaky tests in a separate group and track flake rate. Never let retries hide a real bug.
Q2. How do you guarantee thread-safety in parallel execution?
WebDriver in a ThreadLocal (DriverManager), never a shared static field. No shared mutable state between tests; each test seeds and tears down its own data. Unique data per thread (timestamp/UUID-suffixed users). TestNG parallel="methods" + thread-count. remove() the ThreadLocal on teardown to avoid leaks/bleed. Avoid static caches that tests mutate.
Q3. Selenium vs Playwright — how do you choose? Selenium when: broad/legacy browser matrix, existing Java estate, Grid/BrowserStack already in place, team skilled in it (Candescent is Java/Selenium-leaning). Playwright when: modern SPA, need speed + auto-wait + network interception + trace viewer, TypeScript team, less flake out of the box. Both fit the same page-object architecture — the layered design doesn't change. I'd often keep Selenium for the regression estate and use Playwright for new fast-feedback suites.
Q4. Page Object vs Screenplay pattern? Page Object: pages encapsulate locators + actions; simple, widely understood, great default. Screenplay: actors perform tasks composed of interactions — better separation, more reusable for very large suites, but more boilerplate and a learning curve. For most BFSI teams I'd use Page Objects + a business-action layer (a pragmatic middle ground that gets Screenplay's readability without its complexity). Mention I know Screenplay exists and when it pays off (huge suites, many shared interactions).
Q5. How do you structure a framework for thousands of tests? Strict layering + one page object per screen; group tests (smoke/sanity/regression/by-module) so CI runs the right subset; parallel execution + ThreadLocal; tag-based selection; shared core utils as a versioned internal library/module; data factories not hardcoded data; fail-fast smoke gate on PRs, full regression nightly; keep tests independent so any subset runs in any order; dashboards for flake/duration trends; shard across CI agents.
Q6. CI integration?
Maven build triggered by Jenkins/GitHub Actions. PR pipeline runs smoke (-Dgroups=smoke); merge/nightly runs full regression with -Penv=preprod. Publish Allure history, archive screenshots/logs, post pass/fail + report link to Slack/email. Secrets injected from Jenkins credentials/Vault. Matrix builds for browser/platform combos. Fail the build on regression failures; quarantine group doesn't block.
Q7. Test data management? Prefer API/DB seeding over UI for setup (fast, reliable). Synthetic data via Faker + builders; never reuse mutable shared accounts in parallel. Externalize data (JSON/Excel/DB) from code. For banking: dedicated synthetic test accounts, never real PII; reset/restore state in teardown; idempotent setup so reruns are clean. Data should make tests independent and repeatable.
Q8. Reporting to stakeholders? Two audiences. Engineers: Allure (trends, history, drill-down, attached logs/screenshots/req-resp). Stakeholders/managers: Extent or an Allure summary — a single shareable HTML with pass/fail %, duration, top failures, screenshots. CI posts a one-line summary + link to Slack/email. Track KPIs over time: pass rate, flake rate, execution time, coverage by module. Make failures actionable (screenshot + stack + the request/response).
Q9. How do you keep tests fast?
API-seed state, UI-test only what needs UI; parallelize; run smoke on PRs and full suite nightly; avoid sleep; reuse auth tokens; headless in CI; shard across agents.
Q10. How do you decide what to automate vs leave manual? Automate stable, high-value, repetitive, regression-prone flows (login, transfer, checkout). Leave exploratory, one-off, rapidly-changing UI, and visual/UX judgement to manual. Prioritize by risk × frequency — in BFSI, money-movement and security paths first.
Q11. How do you handle environment-specific differences (e.g., feature flags)?
Config per env (config-{env}.properties), feature-flag-aware tests that skip/branch on flag state queried via API, and env-tagged suites. Never hardcode URLs/creds. Prod automation is read-only / synthetic-account-only.
Q12. How would you validate a money calculation precisely?
Drive it primarily at the API layer with data-driven cases (@DataProvider), use BigDecimal with fixed scale and compareTo, assert to the cent, include rounding edge cases and negative paths (insufficient funds, limit breaches). A couple of UI checks confirm the displayed value matches the backend truth.
8. One-Liners to Drop In the Room (memorable framing)¶
- "My core framework is domain-agnostic — for any new product I only swap the page objects, API services, and business actions; the engine stays."
- "Tests speak business language; locators and waits live below the test layer."
- "API to set up state and assert truth; UI only for what genuinely needs a screen."
- "WebDriver lives in a ThreadLocal — that single decision is what makes parallel runs safe."
- "In BFSI I never compare money with
==ordouble; it'sBigDecimal.compareTo, idempotency keys, and an audit-trail assertion on every transfer." - "Retries are a seatbelt, not a fix — I root-cause flake and track flake rate." ```