Software Development Methodologies — TDD, BDD, ATDD (Candescent SDET Prep)¶
Covers Round 1, Section 5 — "Software Development Methodologies". Model answers to the fixed question set (each with its follow-up), foundations from scratch, a banking Gherkin + Java step-def example, and BFSI/regulated-org notes throughout.
1. Foundations (from scratch)¶
1.1 TDD — Test-Driven Development¶
What it is: A developer discipline where you write a failing unit test first, then write just enough production code to pass it, then refactor. It is a design technique as much as a testing one — it forces small, testable units and good API design.
The cycle — Red → Green → Refactor: 1. Red — write a small failing test for the next bit of behavior. 2. Green — write the minimum code to make it pass (no gold-plating). 3. Refactor — clean up code and tests while staying green. 4. Repeat in tight loops (minutes, not hours).
Level: unit. Driven by: developers. Artifact: fast, isolated unit tests (JUnit/TestNG).
// 1. RED — write the test first, it won't even compile yet
@Test
void debitsAccountWhenSufficientBalance() {
Account acct = new Account(new BigDecimal("100.00"));
acct.debit(new BigDecimal("40.00"));
assertEquals(new BigDecimal("60.00"), acct.getBalance());
}
@Test
void rejectsDebitWhenInsufficientBalance() {
Account acct = new Account(new BigDecimal("30.00"));
assertThrows(InsufficientFundsException.class,
() -> acct.debit(new BigDecimal("40.00")));
}
// 2. GREEN — minimal implementation to pass
public class Account {
private BigDecimal balance;
public Account(BigDecimal opening) { this.balance = opening; }
public void debit(BigDecimal amount) {
if (amount.compareTo(balance) > 0)
throw new InsufficientFundsException();
balance = balance.subtract(amount);
}
public BigDecimal getBalance() { return balance; }
}
// 3. REFACTOR — extract validation, add value objects (Money), keep tests green.
BFSI note: In banking, the unit under test is often money math, interest accrual, fee calculation, rounding, and currency handling — exactly where TDD shines because each rule maps to a precise, auditable test. Always use BigDecimal (never double) for money; TDD makes that rounding contract explicit and regression-proof.
1.2 BDD — Behavior-Driven Development¶
What it is: An evolution of TDD that shifts the conversation from "test this method" to "describe the behavior the system should exhibit", written in business-readable language so that business analysts, developers, and testers share one vocabulary (ubiquitous language). The behavior is expressed as Given–When–Then scenarios, often in Gherkin and automated with Cucumber.
- Given — the context/preconditions.
- When — the action/event.
- Then — the observable outcome.
Level: behavior/feature (can drive unit or acceptance). Driven by: collaboration (the "three amigos"). Artifact: .feature files (Gherkin) + step definitions; doubles as living documentation.
Feature: Domestic fund transfer
As an online-banking customer
I want to transfer money between my accounts
So that I can manage my finances
Scenario: Successful transfer with sufficient funds
Given my checking account balance is 500.00 USD
When I transfer 200.00 USD to my savings account
Then the transfer succeeds
And my checking account balance is 300.00 USD
BFSI note: Gherkin scenarios become audit-friendly living documentation. A compliance reviewer can read a .feature file and confirm the implemented behavior matches the regulatory requirement — no need to read Java. This traceability (requirement ↔ scenario ↔ passing test) is gold in a regulated digital-banking shop.
1.3 ATDD — Acceptance-Test-Driven Development¶
What it is: A collaborative practice where the team writes acceptance tests (acceptance criteria) BEFORE development begins, agreed by business + dev + QA (the "three amigos"). Those acceptance tests define "done" for the story. Development proceeds until the acceptance tests pass.
ATDD vs BDD — the relationship: - Same family, different emphasis. ATDD focuses on what acceptance means — getting business, dev, and QA to agree on concrete examples of "done" before coding. BDD focuses on how to express behavior in a shared, ubiquitous language (Given-When-Then) and on the ongoing conversation. - BDD is often the vehicle that implements ATDD: you do ATDD (agree acceptance criteria up front) and capture them as BDD scenarios in Gherkin. - ATDD acceptance tests can be in any form (a checklist, a spreadsheet, FitNesse, Gherkin). BDD is specifically conversation + Given-When-Then. - Rule of thumb: ATDD = the practice of agreeing acceptance up front; BDD = the language/format often used to express it. In practice teams blur the two.
BFSI note: In banking, acceptance criteria are frequently tied directly to compliance and regulatory rules (e.g., daily transfer limits, AML thresholds, KYC gating, dual authorization). Writing acceptance tests first, with the business stakeholder in the room, is how you guarantee the regulatory intent is captured before a line of code is written.
1.4 Comparison table¶
| Dimension | TDD | BDD | ATDD |
|---|---|---|---|
| Who drives | Developers | Three amigos (BA/Dev/QA), dev-leaning | Three amigos (Business/Dev/QA), business-leaning |
| Level | Unit | Behavior / feature (unit → acceptance) | Acceptance / feature |
| When written | Just before the unit of code, in tight loops | Before/at story refinement, before dev | Before development starts (defines "done") |
| Tooling | JUnit, TestNG, Mockito | Cucumber, Gherkin, SpecFlow, Behave | Cucumber/Gherkin, FitNesse, Robot, or even a criteria checklist |
| Language | Code (test methods) | Business-readable Given-When-Then | Business-readable acceptance criteria/examples |
| Artifact | Fast unit tests | .feature files + step defs (living docs) |
Agreed acceptance tests / criteria |
| Primary benefit | Clean design + safety net at unit level | Shared understanding, readable specs, less ambiguity | Right thing built; "done" agreed up front; fewer requirement defects |
| BFSI value | Auditable money/rule math | Audit-friendly living documentation | Compliance criteria locked before coding |
One-liner to say in interview: "TDD makes sure I build the thing right at the unit level; ATDD makes sure we build the right thing by agreeing acceptance up front; BDD is the shared language we use to describe behavior so business, dev, and QA all read the same spec."
2. Model answers to the JD question set¶
Each answer is structured to be speakable in ~1–2 minutes.
Q1. Differentiate TDD vs BDD vs ATDD in practice. (Follow-up: how to decide which to apply.)¶
Answer: In practice they sit at different altitudes. TDD is a developer-level loop — write a failing unit test, make it pass, refactor — and its real value is design and a fast regression net for logic like fee or interest calculations. ATDD is a team practice: before we code a story, business, dev, and QA agree on concrete acceptance examples that define "done", so we build the right thing. BDD is the shared language — Given-When-Then in Gherkin — that we use to capture behavior so everyone reads the same spec, and it becomes living documentation. They're complementary, not competing: I often do ATDD to agree criteria, write them as BDD scenarios, and use TDD underneath to implement the units.
Follow-up — how to decide which to apply: It depends on the layer and the audience. For complex internal logic with no business audience — a rounding engine, a limit calculator — pure TDD is enough. When a story has business ambiguity or regulatory acceptance criteria, I push for ATDD/BDD so the business stakeholder signs off before coding. On a BFSI product I'd typically run all three: BDD/ATDD at the feature/acceptance layer for traceability and audit, TDD at the unit layer. If the team is junior on collaboration, start with TDD; if requirement defects are the pain, invest in ATDD/BDD.
Q2. How do you implement TDD in your testing workflow? (Follow-up: ensure devs and testers collaborate in TDD.)¶
Answer: I work in the red-green-refactor loop. I take the smallest next behavior, write a failing JUnit/TestNG test that asserts the expected outcome, confirm it fails for the right reason, then write the minimum code to pass, and refactor with the green safety net. I keep tests fast and isolated — mock collaborators (e.g., the ledger gateway) with Mockito so the test only exercises the logic. I commit in small increments, each commit green. As an SDET I also bring an edge-case mindset early: boundary amounts, zero, negative, overflow, currency rounding — so the unit suite is genuinely thorough, not just happy-path.
Follow-up — dev/tester collaboration in TDD: TDD is mostly developer-owned, so the tester's job is to enrich it, not duplicate it. I pair with developers during the "three amigos"/refinement to feed them the edge cases and negative scenarios that should become unit tests. I review their unit tests for missing boundaries and for assertions that actually check behavior, not just "no exception". I make sure the unit layer and my acceptance/BDD layer don't overlap wastefully — devs own fast unit TDD, I own the integration/e2e and behavior layer above it. Shared coverage dashboards and a common definition of done keep both sides aligned.
Q3. How do you write effective BDD scenarios in Gherkin? (Follow-up: avoid too detailed / too vague — declarative vs imperative.)¶
Answer: I write scenarios from the user's intent, one behavior per scenario, in business language. Each has a clear Given (context), When (single action), Then (observable outcome). I keep them declarative — describing what happens, not how I click — so they read like a spec and survive UI changes. I use a Background for common preconditions, Scenario Outline with Examples for data variations like different transfer amounts/limits, and tags for organizing suites. The acid test: a business analyst or auditor should be able to read it and agree it's correct.
Follow-up — too detailed vs too vague (declarative vs imperative): The common failure is imperative Gherkin — "click the Transfer button, type 200 in field X, click Submit" — which is brittle and unreadable. I fix that by raising the abstraction to declarative: "When I transfer 200.00 USD to my savings account". The mechanics live in the step definition, not the feature. The opposite failure is too vague — "When I do a transfer / Then it works" — which isn't verifiable. The balance is concrete business facts (amounts, account types, outcomes) without UI mechanics. One behavior per scenario; if a scenario has many When/Then pairs, I split it.
Q4. How do you use ATDD to align QA, devs, and business stakeholders? (Follow-up: handle conflicts when business expectations don't match technical feasibility.)¶
Answer: ATDD's core ritual is the three-amigos session at story refinement: business explains intent, dev raises technical constraints, QA pokes at edge and negative cases. Together we write concrete acceptance examples before coding — for a transfer feature, that's success, insufficient funds, daily-limit breach, and dual-authorization cases. We capture them as Gherkin scenarios so everyone signs off on the same artifact. Development then targets making those scenarios pass; "done" is unambiguous and agreed. In BFSI this is where I make sure each compliance rule (limits, AML thresholds) becomes an explicit, signed-off acceptance test.
Follow-up — business vs technical feasibility conflicts: I surface the conflict early, in the three-amigos session, before commitment. I make the cost concrete: explain why something is hard (e.g., real-time fraud scoring on every transfer adds latency/cost) and offer feasible alternatives and trade-offs rather than a flat "no". For regulatory criteria there's usually no negotiating the what, so we negotiate the how/when — phasing, a simpler first cut, async processing. The decision and its rationale get documented in the acceptance criteria so it's auditable. The goal is a shared, written agreement, not a winner — and the product owner makes the final call with full information.
Q5. How do you ensure test coverage when following TDD? (Follow-up: gaps unit tests miss — integration, e2e, non-functional.)¶
Answer: At the unit level, TDD gives high coverage almost for free because no code exists without a test that demanded it. I track line/branch coverage (JaCoCo) but treat the number as a guardrail, not a goal — I focus on behavior and boundary coverage: every decision branch, every error path, edge amounts, rounding, nulls. I use mutation testing (PIT) where it matters to confirm the tests actually catch defects, not just execute lines. For banking logic I make sure each business/compliance rule has at least one explicit positive and negative test.
Follow-up — gaps TDD unit tests miss: Unit TDD verifies pieces in isolation, so it can't catch problems between pieces. I fill the gaps with a layered strategy (a test pyramid): integration tests for the real DB, the ledger service, and external gateways (RestAssured against the transfer API, contract tests for service boundaries); end-to-end / BDD acceptance tests for full user journeys (login → transfer → statement) in Playwright/Cucumber; and non-functional coverage — performance/load on the transfer endpoint, security (authz, injection, the OWASP basics critical in BFSI), and resilience/failover. So TDD owns the base of the pyramid; integration, e2e, and NFR testing own the layers TDD structurally cannot see.
Q6. How do you integrate BDD scenarios into automation frameworks? (Follow-up: organize step defs to avoid duplication / improve maintainability.)¶
Answer: I wire Cucumber into the Java build (Maven/Gradle) with a JUnit/TestNG runner. .feature files hold the Gherkin; step definitions are the glue mapping each step to Java code. Crucially, step defs stay thin — they delegate to a layer of page objects (Playwright/Selenium) for UI and API client objects (RestAssured) for services, so business logic and locators live behind the steps, not in them. I use Cucumber hooks (@Before/@After) for setup/teardown and screenshots-on-failure, tags to slice suites (@smoke, @regression, @transfer), and the framework plugs into CI so scenarios run on every pipeline and produce living-doc reports.
Follow-up — avoiding duplication / maintainability: I organize step defs by domain, not by feature file — an AccountSteps, TransferSteps, AuthSteps — so a step like "Given my checking balance is X" exists once and is reused everywhere. I share state across step classes with dependency injection (PicoContainer / Cucumber-Spring) instead of static variables, which keeps steps stateless and parallel-safe. Reusable actions go into the page/API object layer so changing a locator or endpoint touches one place. I keep Gherkin declarative so steps are generic and composable, enforce a glue/style review, and periodically refactor near-duplicate steps. Net effect: features read cleanly, and maintenance cost stays flat as the suite grows.
Q7. How do you measure effectiveness/ROI of adopting BDD/TDD/ATDD? (Follow-up: convince stakeholders when execution time goes up initially.)¶
Answer: I measure with leading and lagging indicators. Defect-related: defect escape rate to UAT/production, defect density, and especially requirement defects (rework caused by misunderstanding) — ATDD/BDD should drive these down. Quality/flow: escaped-defect cost, change-failure rate, mean time to detect, and regression cycle time (automation shrinks it). Coverage & confidence: branch/mutation coverage trends, and how much manual regression we retired. Documentation: living docs reduce onboarding and audit prep time — a real, measurable saving in BFSI. I baseline before adoption and trend after, so the improvement is evidence-based.
Follow-up — convincing stakeholders when execution time rises first: I set expectations that there's an upfront investment — writing tests first and building the harness is slower in sprint one or two — and frame it as a cost-of-quality trade. I quantify the alternative: the cost of a production defect in banking (incident response, regulatory exposure, customer trust, reputational hit) dwarfs the upfront test time. I show the curve: execution/dev time dips short-term, then regression and rework time fall sharply as the safety net pays back, usually within a few sprints. I present concrete data — e.g., escaped defects before vs after, regression time cut from days to hours — and pilot on one module to prove ROI before scaling. In a regulated org, "audit-ready living documentation produced for free" is itself a line-item saving stakeholders feel.
Q8. How do you handle test data and environment setup in ATDD and BDD? (Follow-up: consistent test data across QA/UAT/Production-like environments.)¶
Answer: I keep test data out of the Gherkin's business language and inject it through steps/hooks. Setup happens in Cucumber hooks or fixtures — seed the accounts and balances a scenario needs, then clean up after so tests are independent and repeatable. I prefer programmatic setup via API (create the account/balance through the service) over UI setup because it's fast and stable, and I use builders/factories for test entities. For variations I use Scenario Outline + Examples tables. Environments are configuration, not code — base URLs, credentials, and DB endpoints come from profiles/env files, selected by tag or runtime parameter, never hardcoded. Sensitive data (PII, credentials) is masked/synthetic and pulled from a secrets store, which matters acutely in BFSI.
Follow-up — consistent data across QA/UAT/prod-like: I make data setup self-provisioning and idempotent — each test creates (and tears down) what it needs via API/fixtures rather than relying on pre-existing rows that drift between environments. Where shared reference data is needed, I version it as seed scripts in source control and apply the same script to every environment, so QA, UAT, and prod-like start from a known, identical baseline. I parameterize all environment-specific bits (accounts, endpoints) per profile. For BFSI I use synthetic but realistic data and never copy real production PII into lower environments — that's both a compliance requirement and a way to keep data deterministic. The principle: tests own their data and assume nothing about the environment, so the same scenario passes identically everywhere.
3. Banking Gherkin feature + Java step-definition skeleton¶
3.1 fund_transfer.feature¶
@transfer @regression
Feature: Fund transfer between customer accounts
As an online-banking customer
I want to transfer money between my accounts
So that I can manage my balances safely and within bank limits
Background:
Given I am a logged-in customer "rohan"
And my "Checking" account balance is 1000.00 USD
And my "Savings" account balance is 250.00 USD
And my daily transfer limit is 2000.00 USD
@smoke @happy-path
Scenario: Transfer succeeds when funds are sufficient
When I transfer 300.00 USD from "Checking" to "Savings"
Then the transfer is successful
And my "Checking" balance is 700.00 USD
And my "Savings" balance is 550.00 USD
@negative
Scenario: Transfer is rejected when funds are insufficient
When I transfer 5000.00 USD from "Checking" to "Savings"
Then the transfer is rejected with reason "INSUFFICIENT_FUNDS"
And my "Checking" balance is unchanged at 1000.00 USD
@compliance @limits
Scenario: Transfer is rejected when it breaches the daily limit
Given I have already transferred 1800.00 USD today
When I transfer 300.00 USD from "Checking" to "Savings"
Then the transfer is rejected with reason "DAILY_LIMIT_EXCEEDED"
@data-driven
Scenario Outline: Transfer outcomes for various amounts
When I transfer <amount> USD from "Checking" to "Savings"
Then the transfer result is "<result>"
Examples:
| amount | result |
| 0.01 | SUCCESS |
| 1000.00 | SUCCESS |
| 1000.01 | INSUFFICIENT_FUNDS |
| 0.00 | INVALID_AMOUNT |
| -50.00 | INVALID_AMOUNT |
3.2 Step definitions (good organization: thin steps, DI-shared context, API/page objects behind steps)¶
// ----- World / shared context, injected via PicoContainer or Cucumber-Spring -----
public class TestContext {
public String customerId;
public TransferResult lastResult; // outcome carried between When/Then
public final TransferApiClient transferApi; // API object behind the steps
public final AccountApiClient accountApi;
public TestContext() {
this.transferApi = new TransferApiClient(Config.baseUrl());
this.accountApi = new AccountApiClient(Config.baseUrl());
}
}
// ----- Hooks: environment + data setup/teardown, screenshots on failure -----
public class Hooks {
private final TestContext ctx;
public Hooks(TestContext ctx) { this.ctx = ctx; } // DI
@Before
public void setUp(Scenario scenario) {
// env-config driven; no hardcoded URLs/creds
ctx.accountApi.authenticate(Config.serviceToken());
}
@After
public void tearDown(Scenario scenario) {
ctx.accountApi.cleanUpTestData(ctx.customerId); // self-provisioned data removed
if (scenario.isFailed() && Config.uiEnabled()) {
scenario.attach(Browser.screenshot(), "image/png", "failure");
}
}
}
// ----- AccountSteps: reusable Given steps, organized by DOMAIN not by feature -----
public class AccountSteps {
private final TestContext ctx;
public AccountSteps(TestContext ctx) { this.ctx = ctx; }
@Given("I am a logged-in customer {string}")
public void loggedInCustomer(String name) {
ctx.customerId = ctx.accountApi.createCustomer(name); // programmatic, via API
}
@Given("my {string} account balance is {double} USD")
public void accountBalanceIs(String type, double amount) {
ctx.accountApi.seedAccount(ctx.customerId, type, Money.usd(amount));
}
@Given("my daily transfer limit is {double} USD")
public void dailyLimitIs(double limit) {
ctx.accountApi.setDailyLimit(ctx.customerId, Money.usd(limit));
}
@Given("I have already transferred {double} USD today")
public void alreadyTransferredToday(double amount) {
ctx.accountApi.recordTodayTransfers(ctx.customerId, Money.usd(amount));
}
}
// ----- TransferSteps: the action + assertions; logic lives in the API object -----
public class TransferSteps {
private final TestContext ctx;
public TransferSteps(TestContext ctx) { this.ctx = ctx; }
@When("I transfer {double} USD from {string} to {string}")
public void transfer(double amount, String from, String to) {
ctx.lastResult = ctx.transferApi.transfer(
ctx.customerId, from, to, Money.usd(amount));
}
@Then("the transfer is successful")
public void transferSucceeded() {
assertEquals(TransferResult.Status.SUCCESS, ctx.lastResult.status());
}
@Then("the transfer is rejected with reason {string}")
public void transferRejected(String reason) {
assertEquals(TransferResult.Status.REJECTED, ctx.lastResult.status());
assertEquals(reason, ctx.lastResult.reasonCode());
}
@Then("the transfer result is {string}")
public void transferResultIs(String expected) {
assertEquals(expected, ctx.lastResult.outcomeCode());
}
@Then("my {string} balance is {double} USD")
public void balanceIs(String type, double expected) {
assertEquals(Money.usd(expected),
ctx.accountApi.balanceOf(ctx.customerId, type));
}
@Then("my {string} balance is unchanged at {double} USD")
public void balanceUnchanged(String type, double expected) {
balanceIs(type, expected); // reuse — no duplicate assertion logic
}
}
Why this is well-organized:
- Steps are thin — they delegate to TransferApiClient / AccountApiClient (API objects) or page objects. No HTTP/locator code in the glue.
- State is shared via dependency injection (TestContext), not static fields → parallel-safe, no leakage.
- Steps are grouped by domain (AccountSteps, TransferSteps) so common steps are defined once and reused across features.
- Hooks centralize setup/teardown, environment config, and failure screenshots.
- Reused assertions (balanceUnchanged calls balanceIs) eliminate duplication.
4. Best practices & anti-patterns¶
Do¶
- Declarative over imperative — say what ("I transfer 300 USD to savings"), not how ("click #transferBtn"). Survives UI change, reads as a spec.
- One behavior per scenario — single When/Then intent. Split multi-action scenarios.
Backgroundfor shared preconditions (logged-in user, seeded balances) — keep it short and relevant to every scenario in the file.Scenario Outline+Examplesfor data variation (amounts, limits, currencies) instead of copy-pasted scenarios.- Tags for slicing:
@smoke,@regression,@negative,@compliance, plus environment tags (@uat,@perf) to control what runs where. - Domain-organized step defs + page/API objects behind steps; share state via DI.
- Business/ubiquitous language in Gherkin so analysts and auditors can read and approve it (living documentation).
- Self-provisioning, idempotent test data via API/hooks; synthetic PII only.
- Tie acceptance criteria to compliance rules explicitly (limits, AML, dual auth, KYC) so the audit trail is built-in.
Don't (anti-patterns)¶
- Imperative / UI-coupled Gherkin — clicks, field IDs, waits in the feature file. Brittle and unreadable.
- Vague scenarios — "When I do a transfer / Then it works." Not verifiable.
- Conjunction steps / multiple actions in one scenario — hard to diagnose failures.
- Logic, loops, or assertions inside
.featurefiles — Gherkin is specification, not code. - Duplicated step defs organized per-feature-file → maintenance nightmare. Organize by domain, reuse.
- Static shared state between steps → flaky in parallel runs. Use DI.
- Hardcoded URLs/credentials/data → use env profiles + secrets store.
- Bloated
Backgroundwith setup irrelevant to some scenarios → move to specific Givens. - Treating coverage % as the goal — chase behavior/branch/mutation coverage, not vanity numbers.
- Real production PII in lower environments — compliance violation; use synthetic data.
5. BFSI / regulated-org thread (summary)¶
- Acceptance criteria ↔ compliance: every regulatory rule (daily limits, AML thresholds, dual authorization, KYC gating) is captured as an explicit, signed-off ATDD/BDD scenario before coding. The business stakeholder approves the Gherkin.
- Audit-friendly living documentation: passing
.featurefiles are human-readable proof that implemented behavior matches the requirement — traceability from requirement → scenario → green test, ready for auditors with no code reading. - TDD for money correctness: unit tests pin down rounding, currency, fee, and interest math with
BigDecimal— the most audit-sensitive logic. - Data & environment discipline: synthetic data only in lower environments, secrets from a vault, idempotent self-provisioning so QA/UAT/prod-like behave identically — both a quality and a compliance need.
- ROI in a regulated context: the cost of a production/compliance defect (incident, regulatory exposure, reputational damage) far outweighs the upfront TDD/BDD investment, and the living documentation is itself a saving in audit-prep effort.
60-second recap to memorize¶
"TDD = developer red-green-refactor at the unit level, builds the thing right. ATDD = three-amigos agreeing acceptance up front, builds the right thing. BDD = the Given-When-Then shared language that expresses behavior as living documentation. In BFSI I run all three: TDD for money math, ATDD/BDD for compliance-tied acceptance criteria that double as audit-ready docs — with thin, domain-organized Cucumber steps over API/page objects, DI-shared state, tags for suites, and self-provisioning synthetic test data."