/tdd - Test-Driven Development Workflow
SkillDev toolsEnforce test-driven development with RED→GREEN→REFACTOR cycle and coverage validation
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the /tdd - Test-Driven Development Workflow skill
What this skill tells your AI
The instructions your AI receives, as published by indigoai-us/hq-core in .claude/skills/tdd/SKILL.md and read by ahel’s review.
Develop a feature using strict test-driven development: write failing tests first, implement minimal code, refactor, and verify coverage.
What a good test is: tests verify behavior through public interfaces, read like a specification, and survive refactors.
Task description: $ARGUMENTS
TDD Cycle (MANDATORY)
RED → GREEN → REFACTOR → REPEAT
- RED: Write a failing test FIRST (verify test fails before implementation)
- GREEN: Write minimal code to pass the test (no over-engineering)
- REFACTOR: Improve code quality while keeping tests green
- REPEAT: Continue until feature is complete and coverage meets requirements
Seams — where tests go
A seam is the public boundary you test at: the interface where you observe behavior without reaching inside. Tests live at seams, never against internals.
Test only at pre-agreed seams. Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything — agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
Ask: "What's the public interface, and which seams should we test?"
Process
Step 1: Framework Detection & Setup
Auto-detect the test framework and package manager from the project:
Package managers (check in priority order):
bun.lockb→bun testpnpm-lock.yaml→pnpm testyarn.lock→yarn testpackage-lock.json→npm testCargo.lock→cargo testgo.mod→go test ./...
Test frameworks (infer from project files):
package.jsonwithjest→ Jest (Node/TypeScript)package.jsonwithvitest→ Vitest (Vite-based)pyproject.tomlorrequirements.txt→ pytest (Python)go.mod→ Go'stestingpackageCargo.toml→ Rust's built-in test framework
If no framework detected: Ask the user which test framework and runner to use. Do not proceed without a confirmed test runner.
Store the detected runner as {TEST_CMD} (e.g., bun test, pnpm test, pytest) and use it for all subsequent test commands in this workflow.
Report detected framework and package manager to the user before proceeding.
Step 2: Define Interfaces (Scaffold)
Create the function/class definition with empty implementation:
- Write TypeScript interfaces (or equivalent) for inputs and outputs
- Define the function signature with placeholder implementation
- Include JSDoc comments describing behavior
Example (TypeScript):
/**
* Validates an email address format.
* @param email - The email string to validate
* @returns true if email is valid, false otherwise
* @throws Error if email is null/undefined
*/
export function validateEmail(email: string): boolean {
throw new Error('Not implemented');
}
Step 3: Write Failing Tests (RED Phase)
Read <repo>/CONTEXT.md if present so test names and interface vocabulary match the project's domain language (ties into /domain-modeling).
Write comprehensive test suite BEFORE implementation:
Test types to include:
- Happy path (valid input → expected output)
- Edge cases (empty, null, undefined, boundary values, max length)
- Error conditions (invalid formats, type errors)
- Integration tests (API endpoints, database operations if applicable)
Requirements:
- Tests must FAIL before implementation
- Each test should be independent (no shared state)
- Use meaningful assertion messages
- Run tests and VERIFY RED status (all fail as expected)
Example test structure (Jest/Vitest):
describe('validateEmail', () => {
describe('happy path', () => {
it('accepts valid email addresses', () => {
expect(validateEmail('user@example.com')).toBe(true);
expect(validateEmail('test.name+tag@domain.co.uk')).toBe(true);
});
});
describe('edge cases', () => {
it('rejects empty string', () => {
expect(validateEmail('')).toBe(false);
});
it('rejects email without @', () => {
expect(validateEmail('invalidemail.com')).toBe(false);
});
it('rejects email without domain', () => {
expect(validateEmail('user@')).toBe(false);
});
it('rejects email with spaces', () => {
expect(validateEmail('user @example.com')).toBe(false);
});
it('handles special characters', () => {
expect(validateEmail('user+tag@example.com')).toBe(true);
expect(validateEmail('user.name@sub.domain.com')).toBe(true);
});
});
describe('error conditions', () => {
it('throws error if email is null', () => {
expect(() => validateEmail(null as any)).toThrow();
});
it('throws error if email is undefined', () => {
expect(() => validateEmail(undefined as any)).toThrow();
});
it('rejects excessively long email', () => {
const longEmail = 'a'.repeat(255) + '@example.com';
expect(validateEmail(longEmail)).toBe(false);
});
});
});
Run tests and verify RED:
{TEST_CMD} # Use the detected test runner from Step 1
# Expected: ALL tests FAIL (red output)
# If ANY test passes, you haven't written the test correctly — fix it before proceeding
Step 4: Implement Minimal Code (GREEN Phase)
Write only the minimum code needed to pass the tests:
- No premature optimization
- No extra features beyond test requirements
- Focus on making tests pass
- Keep code simple and readable
Example minimal implementation:
export function validateEmail(email: string): boolean {
if (email === null || email === undefined) {
throw new Error('Email cannot be null or undefined');
}
if (email.length === 0 || email.length > 254) {
return false;
}
// Basic email regex pattern
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
Run tests and verify GREEN:
{TEST_CMD} # Use the detected test runner from Step 1
# Expected: ALL tests PASS (green output)
Step 5: Refactor (IMPROVE Phase)
Improve code quality while keeping tests green:
- Extract constants and magic strings
- Improve variable/function names
- Remove duplication
- Optimize performance (if needed)
- Add comments for complex logic
Requirements:
- Run tests after each refactoring step
- Verify all tests still PASS
- Refactoring should not add new functionality
- Keep in-loop refactors small and local. For deeper structural refactors (moving seams, reshaping modules), hand off to
/reviewrather than expanding this cycle.
Example refactored implementation:
const EMAIL_MAX_LENGTH = 254;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
/**
* Validates an email address format following RFC 5321 basic rules.
* @param email - The email string to validate
* @returns true if email matches expected format, false otherwise
* @throws Error if email is null or undefined
*/
export function validateEmail(email: string): boolean {
if (email == null) {
throw new Error('Email cannot be null or undefined');
}
if (email.length === 0 || email.length > EMAIL_MAX_LENGTH) {
return false;
}
return EMAIL_PATTERN.test(email);
}
Run tests after EACH refactoring step:
{TEST_CMD} # Use the detected test runner from Step 1
# Expected: ALL tests still PASS (green output)
# Refactor one concern at a time. Stop when code is clean and readable.
Step 6: Verify Coverage
Check code coverage and add tests for any uncovered paths:
Coverage requirements by code type:
| Code Type | Minimum Coverage |
|---|---|
| Standard application code | 80% |
| Financial calculations | 100% |
| Authentication/authorization | 100% |
| Security-critical code | 100% |
| Encryption/decryption | 100% |
Run coverage check:
# Jest
npm test -- --coverage
# Vitest
npx vitest run --coverage
# pytest
pytest --cov=. --cov-report=term-missing
# Go
go test -cover ./...
# Rust
cargo tarpaulin --out Html
Acceptance criteria:
- Lines covered: ≥ target percentage
- Branches covered: ≥ target percentage
- Functions covered: ≥ target percentage
- Any uncovered lines documented with rationale (if intentional)
If coverage is below target:
- Identify uncovered code paths
- Write additional tests for those paths
- Run tests again (verify GREEN)
- Re-check coverage
Worked Example: Email Validator (Complete Walkthrough)
RED Phase Output
$ npm test
FAIL src/email-validator.test.ts
validateEmail
happy path
✕ accepts valid email addresses (0ms)
✕ accepts variation formats (1ms)
edge cases
✕ rejects empty string (0ms)
✕ rejects email without @ (1ms)
✕ rejects email without domain (0ms)
✕ rejects email with spaces (0ms)
✕ handles special characters (1ms)
error conditions
✕ throws error if email is null (0ms)
✕ throws error if email is undefined (0ms)
✕ rejects excessively long email (0ms)
Tests: 0 passed, 10 failed, 10 total
GREEN Phase Output (After Implementation)
$ npm test
PASS src/email-validator.test.ts
validateEmail
happy path
✓ accepts valid email addresses (2ms)
✓ accepts variation formats (1ms)
edge cases
✓ rejects empty string (0ms)
✓ rejects email without @ (0ms)
✓ rejects email without domain (1ms)
✓ rejects email with spaces (0ms)
✓ handles special characters (1ms)
error conditions
✓ throws error if email is null (0ms)
✓ throws error if email is undefined (0ms)
✓ rejects excessively long email (0ms)
Tests: 10 passed, 10 total (18ms)
Coverage Report (After Implementation & Refactoring)
$ npm test -- --coverage
------------|----------|----------|----------|----------|-------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered |
------------|----------|----------|----------|----------|-------------|
All files | 100 | 100 | 100 | 100 | |
email-...ts | 100 | 100 | 100 | 100 | |
------------|----------|----------|----------|----------|-------------|
Anti-Patterns to Avoid
- Testing implementation details: Test behavior, not internal state
- Shared test state: Each test must be independent (no
beforeAllside effects) - Insufficient assertions: Verify specific outputs, not just "no error"
- Untested error paths: Include tests for error conditions, not just happy path
- No mocking of external dependencies: Mock API calls, database, file system, etc.
- Skipped tests: Never commit
xit()or.skip— fix the test or remove it - Tautological: The assertion recomputes the expected value the way the code does (
expect(add(a, b)).toBe(a + b), or a snapshot derived by hand the same way) — it passes by construction and can never disagree with the code. Expected values must come from an independent source of truth (a known-good literal, a worked example, the spec). - Horizontal slicing: Writing all tests first, then all implementation. Bulk tests verify imagined behavior and commit you to test structure before you understand the implementation. Work in vertical slices instead — one test → one implementation → repeat, each test a tracer bullet that responds to what the last cycle taught you.
Tips for Success
- Write tests first, always — This forces you to think about the interface before implementation
- Fail fast — The RED phase should show clear test failures before coding
- Minimal code — The simplest code that passes tests is usually the best code
- Refactor fearlessly — Tests are your safety net; they prevent regressions
- Coverage is a tool, not a goal — 80% coverage is a checkpoint, not a target
- Test the interface, not the implementation — Users care about behavior, not how you achieve it
Framework-Specific Commands
Jest (Node.js/TypeScript)
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run coverage
npm test -- --coverage
# Run specific test file
npm test email-validator.test.ts
Vitest (Vite/TypeScript)
# Run all tests
npx vitest run
# Run tests in watch mode
npx vitest
# Run coverage
npx vitest run --coverage
# Run specific test file
npx vitest email-validator.test.ts
pytest (Python)
# Run all tests
pytest
# Run tests in watch mode
pytest --lf (runs last-failed)
# Run coverage
pytest --cov=. --cov-report=term-missing
# Run specific test file
pytest tests/test_email_validator.py
go test (Go)
# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run coverage
go test -cover ./...
# Run specific test
go test -run TestValidateEmail ./...
cargo test (Rust)
# Run all tests
cargo test
# Run tests in release mode
cargo test --release
# Run with coverage (requires tarpaulin)
cargo tarpaulin --out Html
# Run specific test
cargo test validate_email
Success Checklist
After completing the TDD workflow, verify:
- Framework detected correctly
- Tests written BEFORE implementation (RED phase confirmed)
- All tests PASS (GREEN phase confirmed)
- Code refactored for clarity and quality
- Coverage meets requirements (80%+ standard, 100% for critical paths)
- Test suite includes: happy path, edge cases, error conditions
- No shared state between tests
- Meaningful assertion messages
- Code committed with comprehensive test suite
Signals
- GitHub stars
- 84
- Forks
- 15
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
tdd-indigoai-us- Source
- github.com/indigoai-us/hq-core