In traditional software development workflows, tests are authored post-factum—written only after production business logic has already been implemented. This introduces a fundamental architectural defect: engineers inadvertently couple tests to the internal mechanics of pre-existing classes, closures, and state structures (High Coupling to Implementation Details). Consequently, public interfaces (APIs) end up designed from the inward perspective of the service provider rather than the ergonomic perspective of its consumer.
Test-Driven Development (TDD) inverts this dynamic. Writing a failing test first forces the developer to confront interface boundary questions immediately:
TDD functions as an architectural decomposition discipline: complex business requirements are systematically partitioned into discrete, deterministic specifications. This eliminates speculative bloat (YAGNI — You Aren't Gonna Need It) and constrains cognitive overhead throughout implementation.
The formal mechanics of TDD are codified in three foundational laws:
Adhering to these constraints generates a rhythmic micro-cycle: a test authoring step lasting from a few seconds to one or two minutes, followed immediately by minimal source edits and runner verification.
The internal engine of TDD operates as a closed continuous pipeline: the Red-Green-Refactor workflow.
Each phase maintains strict boundaries of responsibility:
+-----------------------------------------------------------------------+
| TDD CYCLE |
+-----------------------------------------------------------------------+
| |
| +-------------------+ +--------------------+ |
| | 1. RED | ----> | 2. GREEN | |
| | Write failing | | Minimal code to | |
| | test specification| | satisfy assertion | |
| +-------------------+ +--------------------+ |
| ^ | |
| | v |
| New | +--------------------+ |
| require| | 3. REFACTOR | |
| ment +---------------- | Eliminate smells | |
| | (tests stay green) | |
| +--------------------+ |
| |
+-----------------------------------------------------------------------+During the Red phase, you write an executable specification for non-existent functionality. The cardinal rule: the test must fail for the exact expected reason.
ReferenceError: calculateDiscount is not defined or a TypeScript compilation error, the requirement boundary is confirmed.AssertionError: expected undefined to be 42), the absence of target functionality is established.The objective of the Green phase is to exit the red failure state as rapidly as possible. Permissible implementation tactics include:
Only when all test suites are completely green does the engineer earn the right to modify production code without fear of regression. The Refactor phase addresses:
We will construct a production-ready token permission evaluator (ScopeEvaluator) using strict TDD discipline in Vitest.
We specify the initial requirement: the validator must reject empty strings and malformed tokens with an explicit custom error.
import { describe, it, expect } from 'vitest';
import { ScopeEvaluator, SecurityTokenError } from './scope-evaluator';
describe('ScopeEvaluator', () => {
it('throws SecurityTokenError when provided with an empty string', () => {
const evaluator = new ScopeEvaluator();
expect(() => evaluator.evaluateToken('', ['read:users'])).toThrow(
SecurityTokenError
);
});
});Running Vitest fails immediately with a TypeScript compilation error: module ./scope-evaluator cannot be resolved. The RED phase succeeds with an expected failure.
We author the minimum code required to turn the suite green.
export class SecurityTokenError extends Error {
constructor(message: string) {
super(message);
this.name = 'SecurityTokenError';
}
}
export class ScopeEvaluator {
evaluateToken(token: string, requiredScopes: readonly string[]): boolean {
if (!token) {
throw new SecurityTokenError('Token cannot be empty');
}
return false;
}
}The test executes and passes green (PASS).
We expand the specification: valid authorization tokens must carry a Bearer prefix and a base64url payload containing granting scopes.
it('returns true when token contains all required scopes', () => {
const evaluator = new ScopeEvaluator();
// Encoded payload: {"sub":"usr_123","scopes":["read:users","write:orders"]}
const payload = Buffer.from(
JSON.stringify({
sub: 'usr_123',
scopes: ['read:users', 'write:orders'],
})
).toString('base64url');
const rawToken = `Bearer ${payload}`;
const hasAccess = evaluator.evaluateToken(rawToken, ['read:users']);
expect(hasAccess).toBe(true);
});
it('returns false when token is missing one or more required scopes', () => {
const evaluator = new ScopeEvaluator();
const payload = Buffer.from(
JSON.stringify({
sub: 'usr_123',
scopes: ['read:users'],
})
).toString('base64url');
const rawToken = `Bearer ${payload}`;
const hasAccess = evaluator.evaluateToken(rawToken, ['read:users', 'admin:all']);
expect(hasAccess).toBe(false);
});The test suite fails (FAIL) because evaluateToken unconditionally returns false.
We implement base64url deserialization and set intersection logic.
interface TokenPayload {
readonly sub: string;
readonly scopes: readonly string[];
}
export class ScopeEvaluator {
evaluateToken(token: string, requiredScopes: readonly string[]): boolean {
if (!token || !token.trim()) {
throw new SecurityTokenError('Token cannot be empty');
}
if (!token.startsWith('Bearer ')) {
throw new SecurityTokenError('Invalid authorization scheme');
}
const base64Payload = token.slice(7);
let parsed: TokenPayload;
try {
const decodedJson = Buffer.from(base64Payload, 'base64url').toString('utf-8');
parsed = JSON.parse(decodedJson) as TokenPayload;
} catch {
throw new SecurityTokenError('Malformed token payload');
}
if (!Array.isArray(parsed.scopes)) {
return false;
}
const tokenScopeSet = new Set(parsed.scopes);
return requiredScopes.every((scope) => tokenScopeSet.has(scope));
}
}All 3 tests turn green (PASS). We now advance to the Refactor phase.
ScopeEvaluator currently violates the Single Responsibility Principle (SRP): it validates headers, decodes base64 strings, handles JSON deserialization, and validates scope authorization. We extract parsing into a dedicated TokenParser without altering observable behavior or breaking a single test.
export class SecurityTokenError extends Error {
constructor(message: string) {
super(message);
this.name = 'SecurityTokenError';
}
}
export interface DecodedToken {
readonly subject: string;
readonly scopes: ReadonlySet<string>;
}
export class TokenParser {
private static readonly BEARER_PREFIX = 'Bearer ';
public static parse(rawToken: string): DecodedToken {
if (!rawToken || !rawToken.trim()) {
throw new SecurityTokenError('Token cannot be empty');
}
if (!rawToken.startsWith(this.BEARER_PREFIX)) {
throw new SecurityTokenError('Invalid authorization scheme');
}
const base64Payload = rawToken.slice(this.BEARER_PREFIX.length);
try {
const json = Buffer.from(base64Payload, 'base64url').toString('utf-8');
const payload = JSON.parse(json) as { sub?: unknown; scopes?: unknown };
if (typeof payload.sub !== 'string' || !Array.isArray(payload.scopes)) {
throw new Error('Invalid token structure');
}
return {
subject: payload.sub,
scopes: new Set(payload.scopes.filter((s): s is string => typeof s === 'string')),
};
} catch (error) {
if (error instanceof SecurityTokenError) throw error;
throw new SecurityTokenError('Malformed token payload');
}
}
}
export class ScopeEvaluator {
public evaluateToken(token: string, requiredScopes: readonly string[]): boolean {
const decoded = TokenParser.parse(token);
return this.hasRequiredScopes(decoded.scopes, requiredScopes);
}
private hasRequiredScopes(
grantedScopes: ReadonlySet<string>,
requiredScopes: readonly string[]
): boolean {
return requiredScopes.every((scope) => grantedScopes.has(scope));
}
}Running Vitest confirms: 100% of tests remain green, while the architecture is decoupled, encapsulated, and extensible.
Authoring assertions that inspect private helper calls or internal instance states (such as examining private properties like evaluator['cachedScopes']). This produces brittle tests that shatter during refactoring despite correct system behavior. Focus exclusively on public API contracts: inputs and outputs.
Immediately moving to the next task the moment a test turns green without analyzing code smells. Technical debt in TDD accumulates here: without disciplined refactoring, the codebase degenerates into a tangled web of ad-hoc conditional branches written solely to appease the test runner.
Writing a monolithic test case that exercises five nested requirements simultaneously. When the test fails, identifying which branch broke is difficult. Keep steps microscopic: one test specifies one distinct increment of functionality.
Constructing complex class hierarchies or abstract generic factories during the Green phase. Write the most direct, straightforward code during Green. Generalize and abstract only during the Refactor phase once multiple passing tests substantiate the need.
Select one discrete behavioral capability. Author a unit test calling the prospective public API and asserting expected results. Execute the runner and verify expected failure.
Write the minimal viable production code needed to turn the test green. Hardcoding initial scalar constants is completely permissible.
Execute the entire test suite. Verify that both the new test and all existing historical tests pass green without regressions.
Review implementation for code smells, naming clarity, and SOLID adherence. Clean the code and ensure the suite remains 100% green throughout.
Commit the validated delta to version control with a descriptive message referencing the verified contract change.