ATDD flips the usual order: QA writes a Playwright acceptance test before the feature exists, the test fails, and the feature is done when the test passes. That single shift moves QA from verifying finished work to defining what "done" means. This article covers the Red-Green-Refactor cycle, how BDD's Given/When/Then format fits the three-amigos workflow, and what changes in QA's day-to-day when developers are already practicing TDD.
The TDD Cycle: Red, Green, Refactor
TDD follows a strict three-step cycle:
1. Red — Write a test that fails (because the feature doesn't exist yet)
2. Green — Write the minimum code to make the test pass
3. Refactor — Clean up the code while keeping tests green
This cycle is repeated for every small piece of functionality.
// Red phase: Write a failing test
test('calculateDiscount applies 10% for code SAVE10', () => {
const result = calculateDiscount(100, 'SAVE10');
expect(result).toBe(90);
});
// Running this test fails: calculateDiscount is not defined// Green phase: Write minimum code to pass
function calculateDiscount(price: number, code: string): number {
if (code === 'SAVE10') {
return price * 0.9;
}
return price;
}
// Now the test passes// Refactor phase: Clean up without breaking the test
const DISCOUNT_CODES: Record<string, number> = {
SAVE10: 0.1,
SAVE20: 0.2,
SAVE50: 0.5,
};
function calculateDiscount(price: number, code: string): number {
const discount = DISCOUNT_CODES[code] || 0;
return price * (1 - discount);
}
// Tests still pass — refactoring didn't break anythingTDD Benefits That Affect QA
Naturally testable code:Code written TDD tends to have smaller functions with clear inputs and outputs — easier to test at every level.
Regression safety:Every behavior is protected by a test. When QA finds a bug, developers can reproduce it with a test, fix it, and the test prevents regression.
Documentation through tests:TDD tests describe what the code should do in specific scenarios. QA can read tests to understand expected behavior.
Earlier bug detection:Issues caught when writing unit tests are orders of magnitude cheaper to fix than issues found in QA or production.
Behavior-Driven Development (BDD)
BDD extends TDD toward the business layer. Where TDD focuses on how code works, BDD focuses on what behavior is required.
The three amigos — developer, QA, and product manager — collaborate to define behavior using Given/When/Then format before writing any code.
Feature: Discount codes
Scenario: Valid discount code reduces price
Given I have a cart with items totaling $100
When I apply discount code "SAVE10"
Then the total should be $90
And the discount amount should be $10
Scenario: Invalid discount code shows error
Given I have a cart with items totaling $100
When I apply discount code "INVALID"
Then I should see "Invalid discount code"
And the total should remain $100
Scenario: Expired discount code shows error
Given I have a cart with items totaling $100
When I apply discount code "EXPIRED20"
Then I should see "This discount code has expired"How QA Can Apply TDD Mindset
Even if you're not writing unit tests, you can apply the TDD mindset.
Write test cases before manual testing begins
Instead of exploratory testing from scratch, write expected behaviors first:
Feature: Password Reset
What I expect to be true:
1. Entering a valid email sends a reset email within 2 minutes
2. Entering an invalid email shows "Email not found"
3. The reset link expires after 24 hours
4. Using an expired link shows "Link expired" with option to request new one
5. After successful reset, old password no longer works
6. Reset link can only be used onceNow you have a test plan before you start testing.
Acceptance Test-Driven Development (ATDD)
Collaborate with developers to write acceptance tests before the feature is built:
// Written before the feature exists
test('checkout with valid card completes purchase', async ({ page }) => {
// Set up: user with items in cart
// ...
// Execute checkout
await page.fill('[data-testid="card-number"]', '4242 4242 4242 4242');
await page.fill('[data-testid="card-expiry"]', '12/28');
await page.fill('[data-testid="card-cvc"]', '123');
await page.click('[data-testid="pay-now"]');
// Expected outcomes
await expect(page).toHaveURL('/order-confirmation');
await expect(page.getByTestId('order-number')).toBeVisible();
await expect(page.getByTestId('success-message')).toContainText('Payment successful');
});These tests fail until the feature is built. When they pass, the feature is done.
When Developers Practice TDD: What QA Should Know
What changes for QA
Unit test coverage already exists:Developers have already tested edge cases at the code level. QA can focus on integration points, user flows, and business logic — not basic function behavior.
Test failures are meaningful:When a TDD team's CI breaks, it's specific. "Function X returns wrong value for edge case Y" — not "the app is broken somewhere."
Refactoring is safer:With comprehensive unit tests, QA doesn't need to retest basic functionality after refactoring — the tests already cover it.
What QA still needs to do
Integration testing:Unit tests test functions in isolation. QA tests how components work together — the database, the API, the frontend, the email system.
User perspective testing:Developers test "does the code work?" QA tests "does the feature make sense to use?"
Edge case discovery:QA exploratory testing finds scenarios that weren't specified. TDD only covers what was thought of when writing tests.
Performance and reliability:TDD doesn't cover response times, concurrent users, or what happens under load.
Practical TDD for Test Automation
When building your test framework, apply TDD principles:
// Instead of writing the full helper first, write a test for what you want it to do
test('login helper should redirect to dashboard after login', async ({ page }) => {
await loginAs(page, 'member'); // This function doesn't exist yet
await expect(page).toHaveURL('/dashboard');
});
// Now write the minimum implementation
async function loginAs(page: Page, role: 'admin' | 'member') {
const credentials = {
admin: { email: 'admin@test.com', password: 'AdminPass1' },
member: { email: 'member@test.com', password: 'MemberPass1' },
};
await page.goto('/login');
await page.fill('[data-testid="email"]', credentials[role].email);
await page.fill('[data-testid="password"]', credentials[role].password);
await page.click('[data-testid="submit"]');
await page.waitForURL('/dashboard');
}This prevents over-engineering. You build exactly what's needed, and the test confirms it works.
The Testing Pyramid in TDD Context
TDD directly shapes the testing pyramid:
/\
/E2E\ ← QA automation (fewer, slower)
/------\
/ Integr.\ ← Integration tests (moderate)
/----------\
/ Unit Tests \ ← TDD output (many, fast)
/--------------\TDD produces the broad base of unit tests. QA builds the integration and E2E layers on top. Together they form a pyramid where most bugs are caught cheaply at the unit level, with a smaller, focused set of expensive E2E tests.
Summary
- TDD cycle: Red (failing test) → Green (make it pass) → Refactor (clean up)
- TDD produces naturally testable, well-structured code
- BDD extends TDD toward business behavior with Given/When/Then scenarios
- ATDD: QA writes acceptance tests before development starts — tests fail until feature is built
- When developers practice TDD, QA shifts focus to integration, user flows, and edge cases
- Apply TDD mindset to test automation: write the test interface first, then implement
TDD and QA are complementary, not competing. TDD catches unit-level bugs early. QA catches integration and user experience issues. Together they produce software that works correctly at every level.
→ See also: Test-Driven Development: A QA Engineer's Guide | Shift-Left Testing: What It Means and How to Practice It | The Test Pyramid Explained for QA Engineers