plugins/dev/skills/discipline/test-driven-development/SKILL.md
Use when writing production code. Enforces RED-GREEN-REFACTOR cycle: write failing test, make it pass, improve design. Prevents test-after development and ensures verified behavior.
npx skillsauth add madappgang/claude-code test-driven-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Iron Law: "NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST"
Use this skill when:
Watch for these patterns that indicate TDD violations:
Violation Detection: If you find yourself saying "I'll test this after I get it working," you're violating TDD.
TDD follows a strict 3-phase workflow:
Objective: Specify desired behavior through a test that fails
Steps:
Example (TypeScript/Jest):
// tests/user-validator.test.ts
describe('UserValidator', () => {
it('rejects email without @ symbol', () => {
const validator = new UserValidator();
const result = validator.validateEmail('invalid-email');
expect(result.isValid).toBe(false);
expect(result.error).toBe('Email must contain @ symbol');
});
});
// Run: npm test
// Result: FAIL - UserValidator is not defined ✓ (correct failure)
Red Phase Complete When: Test fails with expected error message
Objective: Write minimal code to make test pass
Steps:
Example (TypeScript):
// src/user-validator.ts
interface ValidationResult {
isValid: boolean;
error?: string;
}
export class UserValidator {
validateEmail(email: string): ValidationResult {
if (!email.includes('@')) {
return { isValid: false, error: 'Email must contain @ symbol' };
}
return { isValid: true };
}
}
// Run: npm test
// Result: PASS ✓
Green Phase Complete When: Test passes consistently
Objective: Improve code quality while keeping tests green
Steps:
Example (TypeScript - Refactored):
// src/user-validator.ts
interface ValidationResult {
isValid: boolean;
error?: string;
}
export class UserValidator {
private static readonly EMAIL_REQUIRED_CHARS = '@';
private static readonly EMAIL_ERROR = 'Email must contain @ symbol';
validateEmail(email: string): ValidationResult {
if (!this.containsRequiredChars(email)) {
return this.createError(UserValidator.EMAIL_ERROR);
}
return this.createSuccess();
}
private containsRequiredChars(email: string): boolean {
return email.includes(UserValidator.EMAIL_REQUIRED_CHARS);
}
private createError(message: string): ValidationResult {
return { isValid: false, error: message };
}
private createSuccess(): ValidationResult {
return { isValid: true };
}
}
// Run: npm test
// Result: PASS ✓ (behavior unchanged)
Refactor Phase Complete When: Code is clean AND tests still pass
| Anti-pattern | ✗ Wrong Approach | ✓ Correct TDD Approach |
|--------------|------------------|------------------------|
| Test-After | Write calculateTotal() function, then write tests | Write test for calculateTotal(), see it fail, implement function |
| Empty Tests | Write test that always passes: expect(true).toBe(true) | Write test that fails until production code is correct |
| Happy-Path-Only | Test only valid inputs: validateEmail('[email protected]') | Test invalid inputs too: validateEmail('no-at-sign'), validateEmail('') |
| Skip-Simple | Skip test for "obvious" add(a, b) { return a + b } | Write test: expect(add(2, 3)).toBe(5) - bugs hide in "simple" code |
| Change-Then-Test | Modify calculateDiscount(), run app manually, then add test | Write failing test showing bug, modify code until test passes |
Pattern: 1 happy path + 3 edge cases minimum
Example (TypeScript):
describe('calculateDiscount', () => {
it('applies 10% discount to $100 purchase', () => {
expect(calculateDiscount(100, 0.1)).toBe(90);
});
it('returns 0 for negative amounts', () => {
expect(calculateDiscount(-50, 0.1)).toBe(0);
});
it('returns original amount for 0 discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('throws error for discount > 1', () => {
expect(() => calculateDiscount(100, 1.5)).toThrow('Discount must be <= 1');
});
});
Pattern: 1 test per error type + 1 success case
Example (Python/pytest):
def test_divide_by_zero_raises_error():
with pytest.raises(ZeroDivisionError, match="Cannot divide by zero"):
divide(10, 0)
def test_divide_non_numeric_raises_error():
with pytest.raises(TypeError, match="Arguments must be numbers"):
divide("10", 5)
def test_divide_returns_float():
result = divide(10, 3)
assert result == pytest.approx(3.333, rel=1e-3)
Pattern: 1 render test + 3 interaction tests
Example (TypeScript/React Testing Library):
describe('LoginForm', () => {
it('renders email and password inputs', () => {
render(<LoginForm />);
expect(screen.getByLabelText('Email')).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
});
it('shows error for invalid email', async () => {
render(<LoginForm />);
await userEvent.type(screen.getByLabelText('Email'), 'invalid');
await userEvent.click(screen.getByRole('button', { name: 'Login' }));
expect(screen.getByText('Invalid email address')).toBeInTheDocument();
});
it('disables submit button while loading', async () => {
render(<LoginForm onSubmit={async () => await delay(1000)} />);
const submitButton = screen.getByRole('button', { name: 'Login' });
await userEvent.click(submitButton);
expect(submitButton).toBeDisabled();
});
it('calls onSubmit with form data', async () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
await userEvent.click(screen.getByRole('button', { name: 'Login' }));
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
});
});
});
Pattern: 1 success test + 2 error tests
Example (Go/testing):
func TestGetUser(t *testing.T) {
t.Run("returns user for valid ID", func(t *testing.T) {
req := httptest.NewRequest("GET", "/users/123", nil)
w := httptest.NewRecorder()
handler := NewUserHandler(mockUserRepo)
handler.GetUser(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `"id":"123"`)
})
t.Run("returns 404 for non-existent user", func(t *testing.T) {
req := httptest.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
handler := NewUserHandler(mockUserRepo)
handler.GetUser(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
t.Run("returns 400 for invalid ID format", func(t *testing.T) {
req := httptest.NewRequest("GET", "/users/invalid", nil)
w := httptest.NewRecorder()
handler := NewUserHandler(mockUserRepo)
handler.GetUser(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
Scenario: User reports: "App crashes when searching with empty query"
Step 1 (RED): Write test that reproduces bug
// tests/search.test.ts
describe('searchProducts', () => {
it('returns empty array for empty query', () => {
const result = searchProducts('');
expect(result).toEqual([]);
});
});
// Run: npm test
// Result: FAIL - TypeError: Cannot read property 'toLowerCase' of undefined
Step 2 (GREEN): Fix the bug
// src/search.ts
export function searchProducts(query: string): Product[] {
if (!query || query.trim() === '') {
return [];
}
return products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()));
}
// Run: npm test
// Result: PASS ✓
Step 3 (REFACTOR): Add more edge cases
describe('searchProducts', () => {
it('returns empty array for empty query', () => {
expect(searchProducts('')).toEqual([]);
});
it('returns empty array for whitespace-only query', () => {
expect(searchProducts(' ')).toEqual([]);
});
it('is case-insensitive', () => {
expect(searchProducts('LAPTOP')).toHaveLength(3);
});
});
Scenario: Add email validation to user registration
Step 1 (RED): Write test for feature that doesn't exist
# tests/test_user.py
def test_user_creation_validates_email():
with pytest.raises(ValidationError, match="Invalid email format"):
User.create(email="not-an-email", password="secret123")
# Run: pytest
# Result: FAIL - ValidationError not raised
Step 2 (GREEN): Implement validation
# src/user.py
import re
class ValidationError(Exception):
pass
class User:
@classmethod
def create(cls, email: str, password: str):
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
raise ValidationError("Invalid email format")
return cls(email, password)
# Run: pytest
# Result: PASS ✓
Step 3 (REFACTOR): Extract validation logic
# src/validators.py
class EmailValidator:
PATTERN = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
@classmethod
def validate(cls, email: str) -> bool:
return bool(re.match(cls.PATTERN, email))
# src/user.py
class User:
@classmethod
def create(cls, email: str, password: str):
if not EmailValidator.validate(email):
raise ValidationError("Invalid email format")
return cls(email, password)
# Run: pytest
# Result: PASS ✓ (behavior unchanged)
Scenario: Implement POST /api/orders endpoint
Step 1 (RED): Write test for non-existent endpoint
// handlers/orders_test.go
func TestCreateOrder(t *testing.T) {
t.Run("creates order with valid data", func(t *testing.T) {
payload := `{"user_id": "123", "items": [{"product_id": "456", "quantity": 2}]}`
req := httptest.NewRequest("POST", "/api/orders", strings.NewReader(payload))
w := httptest.NewRecorder()
handler := NewOrderHandler(mockOrderRepo)
handler.CreateOrder(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Contains(t, w.Body.String(), `"order_id"`)
})
}
// Run: go test
// Result: FAIL - handler.CreateOrder undefined
Step 2 (GREEN): Implement handler
// handlers/orders.go
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
order, err := h.orderRepo.Create(req.UserID, req.Items)
if err != nil {
http.Error(w, "Failed to create order", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"order_id": order.ID})
}
// Run: go test
// Result: PASS ✓
Step 3 (REFACTOR): Add error handling tests
func TestCreateOrder(t *testing.T) {
t.Run("creates order with valid data", func(t *testing.T) {
// ... existing test
})
t.Run("returns 400 for invalid JSON", func(t *testing.T) {
req := httptest.NewRequest("POST", "/api/orders", strings.NewReader("invalid"))
w := httptest.NewRecorder()
handler.CreateOrder(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
t.Run("returns 400 for missing user_id", func(t *testing.T) {
payload := `{"items": [{"product_id": "456", "quantity": 2}]}`
req := httptest.NewRequest("POST", "/api/orders", strings.NewReader(payload))
w := httptest.NewRecorder()
handler.CreateOrder(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
This skill integrates with:
Objection: "TDD is too slow, I can code faster without tests" Response: Writing tests first actually saves time by catching bugs early. Debugging later is far more expensive than preventing bugs upfront.
Objection: "This code is too simple to test" Response: Simple code is fastest to test. If it's truly simple, the test takes 30 seconds. If you can't write a fast test, the code isn't simple.
Objection: "I'll write tests after I figure out the design" Response: Tests ARE the design. Writing tests first forces you to think about API usability before implementation locks you in.
Objection: "I need to see if my approach works before committing to tests" Response: That's what the RED phase is for - write a test describing your desired approach, then implement it. If approach changes, update test first.
Core Principle: Tests first, code second, refactor third.
Workflow:
Benefits:
Remember: If you wrote production code without a failing test first, you violated TDD. Delete the code and start over with a test.
testing
A test skill for validation testing. Use when testing skill parsing and validation logic.
tools
--- name: bad-skill description: This skill has invalid YAML in frontmatter allowed-tools: [invalid, array, syntax prerequisites: not-an-array --- # Bad Skill This skill has malformed frontmatter that should fail parsing. The YAML has: - Unclosed array bracket - Wrong type for prerequisites (should be array, not string)
tools
Plugin release process for MAG Claude Plugins marketplace. Covers version bumping, marketplace.json updates, git tagging, and common mistakes. Use when releasing new plugin versions or troubleshooting update issues.
testing
Fetch trending programming models from OpenRouter rankings. Use when selecting models for multi-model review, updating model recommendations, or researching current AI coding trends. Provides model IDs, context windows, pricing, and usage statistics from the most recent week.