Marketplace tdd-enforcer
Use when implementing new features. Enforces TDD workflow - write tests FIRST, then implementation. Ensures AAA pattern, proper coverage, and quality test design.
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/barnhardt-enterprises-inc/tdd-enforcer" ~/.claude/skills/aiskillstore-marketplace-tdd-enforcer && rm -rf "$T"
manifest:
skills/barnhardt-enterprises-inc/tdd-enforcer/SKILL.mdsource content
TDD Workflow Enforcer
When to Use
- Implementing new features
- Adding functionality
- Fixing bugs
- Refactoring code
TDD Process (MANDATORY)
1. Write Tests FIRST (RED Phase)
- Define behavior through tests
- Use AAA pattern (Arrange, Act, Assert)
- Tests MUST fail initially
- Clear test names describe expected behavior
2. Verify Tests Fail (Confirmation)
- Run tests:
npm test - Confirm failure for the RIGHT reason
- Test should fail because feature doesn't exist, not because of syntax error
3. Write Implementation (GREEN Phase)
- Write minimal code to pass tests
- No gold plating or extra features
- Focus solely on making tests pass
4. Verify Tests Pass (Validation)
- Run tests:
npm test - All new tests must be green
- All existing tests must still pass
5. Refactor (REFACTOR Phase)
- Improve code quality
- Remove duplication
- Enhance readability
- Tests stay green throughout
Coverage Requirements
- Overall: 75%+
- Business Logic (src/services/): 90%+
- Utilities (src/utils/): 90%+
- UI Components: 60%+
- E2E tests for critical user flows
AAA Pattern (Arrange, Act, Assert)
describe('AuthService', () => { describe('register', () => { it('should create user with hashed password', async () => { // ARRANGE: Setup test data const userData = { email: 'test@example.com', password: 'Pass123!', } // ACT: Execute the behavior const result = await authService.register(userData) // ASSERT: Verify outcome expect(result.id).toBeDefined() expect(result.email).toBe(userData.email) expect(result).not.toHaveProperty('password') // Never return password }) it('should reject weak passwords', async () => { // ARRANGE const userData = { email: 'test@example.com', password: '123', // Too weak } // ACT & ASSERT await expect(authService.register(userData)).rejects.toThrow( 'Password must be at least 8 characters' ) }) }) })
Test Structure
Describe Blocks
// ✅ DO: Organize by module/class describe('UserService', () => { // ✅ DO: Organize by method describe('findById', () => { it('should return user when found', () => {}) it('should return null when not found', () => {}) it('should throw error for invalid id', () => {}) }) describe('create', () => { it('should create user with valid data', () => {}) it('should validate email format', () => {}) it('should hash password before saving', () => {}) }) })
Test Names
// ✅ DO: Descriptive test names it('should return 400 when email is invalid', () => {}) it('should hash password with bcrypt before saving', () => {}) it('should send welcome email after registration', () => {}) // ❌ DON'T: Vague test names it('works', () => {}) it('test user creation', () => {}) it('should work correctly', () => {})
Testing Different Layers
Unit Tests (Business Logic)
// src/services/auth.service.test.ts import { AuthService } from './auth.service' import { prismaMock } from '../test/prisma-mock' import bcrypt from 'bcrypt' describe('AuthService', () => { describe('login', () => { it('should return user and token for valid credentials', async () => { // ARRANGE const hashedPassword = await bcrypt.hash('password123', 10) const mockUser = { id: '1', email: 'user@test.com', password: hashedPassword, } prismaMock.user.findUnique.mockResolvedValue(mockUser) // ACT const result = await authService.login({ email: 'user@test.com', password: 'password123', }) // ASSERT expect(result.user.email).toBe('user@test.com') expect(result.token).toBeDefined() expect(result.user).not.toHaveProperty('password') }) it('should throw error for wrong password', async () => { // ARRANGE const hashedPassword = await bcrypt.hash('password123', 10) const mockUser = { id: '1', email: 'user@test.com', password: hashedPassword, } prismaMock.user.findUnique.mockResolvedValue(mockUser) // ACT & ASSERT await expect( authService.login({ email: 'user@test.com', password: 'wrongpassword', }) ).rejects.toThrow('Invalid credentials') }) }) })
Integration Tests (API Routes)
// src/app/api/auth/register/route.test.ts import { POST } from './route' describe('POST /api/auth/register', () => { it('should create user and return 201', async () => { // ARRANGE const request = new Request('http://localhost/api/auth/register', { method: 'POST', body: JSON.stringify({ email: 'newuser@test.com', password: 'SecurePass123!', name: 'Test User', }), }) // ACT const response = await POST(request) const data = await response.json() // ASSERT expect(response.status).toBe(201) expect(data.user.email).toBe('newuser@test.com') expect(data.token).toBeDefined() expect(data.user).not.toHaveProperty('password') }) it('should return 400 for invalid email', async () => { // ARRANGE const request = new Request('http://localhost/api/auth/register', { method: 'POST', body: JSON.stringify({ email: 'invalid-email', password: 'SecurePass123!', }), }) // ACT const response = await POST(request) const data = await response.json() // ASSERT expect(response.status).toBe(400) expect(data.error).toContain('email') }) })
Component Tests (UI)
// src/components/LoginForm.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { LoginForm } from './LoginForm' describe('LoginForm', () => { it('should call onSubmit with email and password', async () => { // ARRANGE const mockOnSubmit = vi.fn().mockResolvedValue(undefined) render(<LoginForm onSubmit={mockOnSubmit} />) // ACT fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'user@test.com' }, }) fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' }, }) fireEvent.click(screen.getByRole('button', { name: /login/i })) // ASSERT await waitFor(() => { expect(mockOnSubmit).toHaveBeenCalledWith({ email: 'user@test.com', password: 'password123', }) }) }) it('should display error message when login fails', async () => { // ARRANGE const mockOnSubmit = vi .fn() .mockRejectedValue(new Error('Invalid credentials')) render(<LoginForm onSubmit={mockOnSubmit} />) // ACT fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'user@test.com' }, }) fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'wrongpassword' }, }) fireEvent.click(screen.getByRole('button', { name: /login/i })) // ASSERT await waitFor(() => { expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument() }) }) it('should disable submit button while loading', async () => { // ARRANGE const mockOnSubmit = vi .fn() .mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100))) render(<LoginForm onSubmit={mockOnSubmit} />) // ACT fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'user@test.com' }, }) fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' }, }) const submitButton = screen.getByRole('button', { name: /login/i }) fireEvent.click(submitButton) // ASSERT expect(submitButton).toBeDisabled() await waitFor(() => { expect(submitButton).not.toBeDisabled() }) }) })
E2E Tests (Critical Flows)
// tests/e2e/auth.spec.ts import { test, expect } from '@playwright/test' test.describe('Authentication Flow', () => { test('user can register and login', async ({ page }) => { // ARRANGE const email = `test-${Date.now()}@example.com` const password = 'SecurePass123!' // ACT: Register await page.goto('/register') await page.fill('[name="email"]', email) await page.fill('[name="password"]', password) await page.fill('[name="confirmPassword"]', password) await page.click('button[type="submit"]') // ASSERT: Redirected to dashboard await expect(page).toHaveURL('/dashboard') await expect(page.locator('h1')).toContainText('Dashboard') // ACT: Logout await page.click('[data-testid="user-menu"]') await page.click('text=Logout') // ASSERT: Redirected to login await expect(page).toHaveURL('/login') // ACT: Login await page.fill('[name="email"]', email) await page.fill('[name="password"]', password) await page.click('button[type="submit"]') // ASSERT: Back to dashboard await expect(page).toHaveURL('/dashboard') }) })
Test Quality Requirements
✅ DO: Test behavior, not implementation
// ✅ DO it('should display error message when login fails', async () => { // Test what the user sees await expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument() }) // ❌ DON'T it('should call setError with "Invalid credentials"', async () => { // Testing implementation detail expect(setError).toHaveBeenCalledWith('Invalid credentials') })
✅ DO: Test edge cases
it('should handle empty input', () => {}) it('should handle very long input (> 1000 chars)', () => {}) it('should handle special characters in email', () => {}) it('should handle concurrent requests', () => {})
✅ DO: Test error conditions
it('should handle database connection failure', () => {}) it('should handle network timeout', () => {}) it('should handle invalid JSON response', () => {})
✅ DO: Use test data builders
// Test data builders for cleaner tests const userBuilder = { default: () => ({ email: 'test@example.com', password: 'Pass123!', name: 'Test User', }), withEmail: (email: string) => ({ ...userBuilder.default(), email, }), withoutName: () => ({ email: 'test@example.com', password: 'Pass123!', }), } it('should create user with default data', () => { const user = userBuilder.default() // ... }) it('should create user without name', () => { const user = userBuilder.withoutName() // ... })
Coverage Verification
# Run tests with coverage npm run test:coverage # Check coverage thresholds npm test -- --coverage --coverageThreshold='{"global":{"lines":75,"functions":75,"branches":75}}'
Common TDD Mistakes
❌ DON'T: Write implementation first
// Wrong order 1. Write function 2. Write tests 3. Tests pass (or fix tests to pass)
✅ DO: Write tests first
// Correct order (TDD) 1. Write test (RED) 2. Verify test fails 3. Write minimal implementation (GREEN) 4. Verify test passes 5. Refactor (REFACTOR)
❌ DON'T: Test implementation details
// Bad: Testing internal state expect(component.state.loading).toBe(true) // Good: Testing observable behavior expect(screen.getByTestId('spinner')).toBeInTheDocument()
❌ DON'T: Write one giant test
// Bad: One test does everything it('should handle entire user flow', () => { // 100 lines of test code }) // Good: Split into focused tests it('should validate email format', () => {}) it('should hash password', () => {}) it('should create user in database', () => {}) it('should send welcome email', () => {})
Checklist Before Committing
- All new features have tests written FIRST
- Tests failed initially (RED)
- Implementation makes tests pass (GREEN)
- Code refactored for quality (REFACTOR)
- Coverage thresholds met (75%+ overall, 90%+ business logic)
- All tests use AAA pattern
- Test names are descriptive
- Edge cases tested
- Error conditions tested
- E2E tests for critical user flows