Claude Code Plugins

Community-maintained marketplace

Feedback

Use when implementing new features. Enforces TDD workflow - write tests FIRST, then implementation. Ensures AAA pattern, proper coverage, and quality test design.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name tdd-enforcer
description Use when implementing new features. Enforces TDD workflow - write tests FIRST, then implementation. Ensures AAA pattern, proper coverage, and quality test design.
allowed-tools Read, Grep, Bash

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