Claude
Skills
Sign in
Back

tdd-enforcer

Included with Lifetime
$97 forever

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

Design

What this skill does


# 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)

```typescript
describe('AuthService', () => {
  describe('register', () => {
    it('should create user with hashed password', async () => {
      // ARRANGE: Setup test data
      const userData = {
        email: '[email protected]',
        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: '[email protected]',
        password: '123', // Too weak
      }

      // ACT & ASSERT
      await expect(authService.register(userData)).rejects.toThrow(
        'Password must be at least 8 characters'
      )
    })
  })
})
```

## Test Structure

### Describe Blocks
```typescript
// ✅ 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
```typescript
// ✅ 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)

```typescript
// 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: '[email protected]',
        password: hashedPassword,
      }
      prismaMock.user.findUnique.mockResolvedValue(mockUser)

      // ACT
      const result = await authService.login({
        email: '[email protected]',
        password: 'password123',
      })

      // ASSERT
      expect(result.user.email).toBe('[email protected]')
      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: '[email protected]',
        password: hashedPassword,
      }
      prismaMock.user.findUnique.mockResolvedValue(mockUser)

      // ACT & ASSERT
      await expect(
        authService.login({
          email: '[email protected]',
          password: 'wrongpassword',
        })
      ).rejects.toThrow('Invalid credentials')
    })
  })
})
```

### Integration Tests (API Routes)

```typescript
// 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: '[email protected]',
        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('[email protected]')
    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)

```typescript
// 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: '[email protected]' },
    })
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'password123' },
    })
    fireEvent.click(screen.getByRole('button', { name: /login/i }))

    // ASSERT
    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        email: '[email protected]',
        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: '[email protected]' },
    })
    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: '[email protected]' },
    })
    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 (Critica

Related in Design