by thetestingacademy
Test-driven development workflow patterns with red-green-refactor cycle and refactoring techniques
npx @qaskills/cli add tdd-patternsAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
This skill makes an AI agent develop features test-first: write exactly one failing test, watch it fail for the right reason, write the minimum production code to pass, then refactor while green. It enforces the discipline most "TDD" sessions skip — never writing production code without a failing test demanding it. Trigger it when the user asks for TDD, test-first development, or when implementing new logic in a codebase that already has a test runner wired up.
src/.rejects expired coupons at the boundary minute tells the next reader the rule; test_coupon_3 tells them nothing.Feature: a PriceCalculator that applies tiered bulk discounts.
RED — write the smallest failing test:
// src/price-calculator.test.ts
import { describe, expect, it } from '@jest/globals';
import { calculateTotal } from './price-calculator';
describe('calculateTotal', () => {
it('returns unit price times quantity with no discount under 10 units', () => {
// Arrange
const unitPrice = 4.0;
const quantity = 3;
// Act
const total = calculateTotal(unitPrice, quantity);
// Assert
expect(total).toBe(12.0);
});
});
npx jest price-calculator
# FAIL — Cannot find module './price-calculator' <- failing for the RIGHT reason
GREEN — minimum code, no speculation:
// src/price-calculator.ts
export function calculateTotal(unitPrice: number, quantity: number): number {
return unitPrice * quantity;
}
RED again — the next test forces the discount rule:
it('applies a 10 percent discount at 10 units or more', () => {
expect(calculateTotal(4.0, 10)).toBe(36.0); // 40 - 10%
});
it('applies a 20 percent discount at 50 units or more', () => {
expect(calculateTotal(2.0, 50)).toBe(80.0); // 100 - 20%
});
GREEN:
export function calculateTotal(unitPrice: number, quantity: number): number {
const subtotal = unitPrice * quantity;
if (quantity >= 50) return subtotal * 0.8;
if (quantity >= 10) return subtotal * 0.9;
return subtotal;
}
REFACTOR — under green, extract the tier table:
const DISCOUNT_TIERS: ReadonlyArray<{ minQty: number; multiplier: number }> = [
{ minQty: 50, multiplier: 0.8 },
{ minQty: 10, multiplier: 0.9 },
{ minQty: 0, multiplier: 1.0 },
];
export function calculateTotal(unitPrice: number, quantity: number): number {
const tier = DISCOUNT_TIERS.find((t) => quantity >= t.minQty)!;
return unitPrice * quantity * tier.multiplier;
}
Run the suite after the refactor. Still green, behavior unchanged, structure improved. That is one complete cycle.
Feature: a password strength validator, driven boundary-first.
# tests/test_password_policy.py
import pytest
from app.password_policy import validate
class TestValidate:
def test_rejects_passwords_shorter_than_12_chars(self):
# Arrange / Act
result = validate("Short1!aaaa") # 11 chars
# Assert
assert result.ok is False
assert "at least 12 characters" in result.errors
def test_accepts_a_12_char_password_meeting_all_rules(self):
result = validate("Sturdy-Pass1") # exactly 12
assert result.ok is True
assert result.errors == []
pytest tests/test_password_policy.py -x
# ModuleNotFoundError: No module named 'app.password_policy' <- correct red
Minimum green:
# app/password_policy.py
from dataclasses import dataclass, field
@dataclass
class Result:
ok: bool
errors: list[str] = field(default_factory=list)
def validate(password: str) -> Result:
if len(password) < 12:
return Result(ok=False, errors=["at least 12 characters"])
return Result(ok=True)
Next red drives the remaining rules — and parametrize keeps each rule one logical test:
@pytest.mark.parametrize(
("password", "missing"),
[
("alllowercase-12", "an uppercase letter"),
("ALLUPPERCASE-12", "a lowercase letter"),
("NoDigitsHere-Ab", "a digit"),
],
)
def test_reports_each_missing_character_class(self, password, missing):
result = validate(password)
assert result.ok is False
assert missing in result.errors
Green, then refactor the rule checks into a table of (predicate, message) pairs — same move as the discount tiers above.
When one example lets you fake it (return 36.0), add a second example with different inputs. Two data points force the general implementation; that is exactly when to generalize, not before.
Every test reads as three blocks separated by blank lines. One Act per test. If you need a second Act, you need a second test.
Before starting, jot the behaviors as comments; convert one at a time into a real failing test:
// TODO test list — price-calculator
// [x] no discount under 10 units
// [x] 10% at 10+
// [x] 20% at 50+
// [ ] rejects negative quantity with RangeError
// [ ] rounds to 2 decimal places (0.1 + 0.2 money bugs)
Write the boundary test (exactly 10 units, exactly 12 chars) before the comfortable middle. Off-by-one bugs live at boundaries; TDD that skips them certifies nothing.
jest price-calculator --watch, pytest -x -k password); run the full suite before commit.git commit after each cycle gives you a bisectable history and a free undo for failed refactors.expect(repo.save).toHaveBeenCalled() as the only assertion). Verify observable outcomes; interaction-only tests pass while behavior is broken.- name: Install QA Skills
run: npx @qaskills/cli add tdd-patterns10 of 29 agents supported
Build AI agents that write, run, and fix tests. Playwright, LLM evals, and CI in one live cohort.
Use code AITESTER at checkout