Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing new features. Enforces TDD workflow - write tests FIRST, then implementation. Ensures AAA pattern, proper coverage, and quality test design.
.claude/skills/aiskillstore-tdd-enforcer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 92% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 140% | 0% |
npm testnpm testtypescriptdescribe('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' ) }) }) })
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', () => {}) }) })
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', () => {})
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: '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') }) }) })
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: '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') }) })
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: '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() }) }) })
typescript// 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') }) })
typescript// ✅ 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') })
typescriptit('should handle empty input', () => {}) it('should handle very long input (> 1000 chars)', () => {}) it('should handle special characters in email', () => {}) it('should handle concurrent requests', () => {})
typescriptit('should handle database connection failure', () => {}) it('should handle network timeout', () => {}) it('should handle invalid JSON response', () => {})
typescript// 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() // ... })
bash# Run tests with coverage npm run test:coverage # Check coverage thresholds npm test -- --coverage --coverageThreshold='{"global":{"lines":75,"functions":75,"branches":75}}'
typescript// Wrong order 1. Write function 2. Write tests 3. Tests pass (or fix tests to pass)
typescript// Correct order (TDD) 1. Write test (RED) 2. Verify test fails 3. Write minimal implementation (GREEN) 4. Verify test passes 5. Refactor (REFACTOR)
typescript// Bad: Testing internal state expect(component.state.loading).toBe(true) // Good: Testing observable behavior expect(screen.getByTestId('spinner')).toBeInTheDocument()
typescript// 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', () => {})
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 12,127 | 13,334 | +10% | 1 | 1 | 0% | 2,623 | 6,178 | +136% | 0 | 0 | — |
case-06 | fail→pass | 26,633 | 13,271 | -50% | 1 | 1 | 0% | 3,202 | 6,458 | +102% | 0 | 0 | — |
case-07 | pass→pass | 20,290 | 11,766 | -42% | 1 | 1 | 0% | 2,632 | 5,255 | +100% | 0 | 0 | — |
case-01 | fail→pass | 18,837 | 23,233 | +23% | 1 | 1 | 0% | 4,011 | 7,693 | +92% | 0 | 0 | — |
case-02 | fail→pass | 15,326 | 17,568 | +15% | 1 | 1 | 0% | 2,667 | 5,812 | +118% | 0 | 0 | — |
case-03 | pass→fail | 12,167 | 13,284 | +9% | 1 | 1 | 0% | 2,396 | 5,176 | +116% | 0 | 0 | — |
case-04 | pass→pass | 12,135 | 11,787 | -3% | 1 | 1 | 0% | 1,415 | 4,690 | +231% | 0 | 0 | — |
case-08 | fail→pass | 17,533 | 12,875 | -27% | 1 | 1 | 0% | 1,951 | 4,833 | +148% | 0 | 0 | — |
case-09 | pass→pass | 14,100 | 3,699 | -74% | 1 | 1 | 0% | 1,543 | 4,069 | +164% | 0 | 0 | — |
case-10 | pass→pass | 13,162 | 7,765 | -41% | 1 | 1 | 0% | 1,481 | 3,914 | +164% | 0 | 0 | — |
case-11 | pass→pass | 24,186 | 21,350 | -12% | 1 | 1 | 0% | 3,674 | 6,888 | +87% | 0 | 0 | — |
case-12 | pass→pass | 18,366 | 22,052 | +20% | 1 | 1 | 0% | 2,440 | 6,071 | +149% | 0 | 0 | — |
case-13 | fail→fail | 22,983 | 23,269 | +1% | 1 | 1 | 0% | 3,219 | 6,565 | +104% | 0 | 0 | — |
case-14 | pass→pass | 16,922 | 18,593 | +10% | 1 | 1 | 0% | 2,506 | 6,173 | +146% | 0 | 0 | — |
case-15 | fail→pass | 20,848 | 8,381 | -60% | 1 | 1 | 0% | 2,126 | 5,104 | +140% | 0 | 0 | — |
case-16 | pass→pass | 16,534 | 11,217 | -32% | 1 | 1 | 0% | 1,680 | 4,442 | +164% | 0 | 0 | — |
case-17 | pass→pass | 43,375 | 10,300 | -76% | 1 | 1 | 0% | 1,567 | 5,046 | +222% | 0 | 0 | — |
case-18 | pass→pass | 9,180 | 28,026 | +205% | 1 | 1 | 0% | 1,477 | 4,071 | +176% | 0 | 0 | — |
case-19 | pass→fail | 19,275 | 41,804 | +117% | 1 | 1 | 0% | 2,276 | 4,351 | +91% | 0 | 0 | — |
case-20 | pass→pass | 15,055 | 34,609 | +130% | 1 | 1 | 0% | 2,591 | 6,418 | +148% | 0 | 0 | — |
case-21 | pass→fail | 24,433 | 20,243 | -17% | 1 | 1 | 0% | 2,934 | 7,295 | +149% | 0 | 0 | — |
case-22 | fail→pass | 29,431 | 13,124 | -55% | 1 | 1 | 0% | 2,291 | 4,903 | +114% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases. 3 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.