Back to list
Wania-Kazmi

testing-patterns

by Wania-Kazmi

Autonomous project generator for Claude Code. Write requirements, run one command, get a complete project with custom skills, agents, hooks, TDD, 80%+ coverage, and security-reviewed code.

2🍴 0📅 Jan 24, 2026

SKILL.md


name: testing-patterns description: | Comprehensive testing patterns for Jest, Vitest, Playwright, and React Testing Library. Use when writing unit tests, integration tests, or E2E tests. allowed-tools: Read, Write, Edit, Bash, Grep, Glob

Testing Patterns & Best Practices

Testing Principles

1. Test Behavior, Not Implementation

  • Test what the code does, not how it does it
  • Tests shouldn't break when refactoring
  • Focus on inputs and outputs

2. Arrange-Act-Assert (AAA)

// Arrange - Set up test data
const user = { name: 'John', email: 'john@example.com' }

// Act - Execute the code
const result = validateUser(user)

// Assert - Verify the outcome
expect(result.isValid).toBe(true)

3. Test Isolation

  • Each test should be independent
  • No shared state between tests
  • Clean up after each test

Unit Testing Patterns

Basic Test Structure

describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      const userData = { name: 'John', email: 'john@example.com' }
      const result = await userService.createUser(userData)
      
      expect(result).toMatchObject({
        id: expect.any(String),
        name: 'John',
        email: 'john@example.com'
      })
    })

    it('should throw error for invalid email', async () => {
      const userData = { name: 'John', email: 'invalid' }
      
      await expect(userService.createUser(userData))
        .rejects
        .toThrow('Invalid email format')
    })
  })
})

Mocking Patterns

// Mock a module
jest.mock('./database', () => ({
  query: jest.fn()
}))

// Mock implementation
const mockQuery = jest.mocked(query)
mockQuery.mockResolvedValue([{ id: '1', name: 'Test' }])

// Spy on method
const spy = jest.spyOn(userService, 'sendEmail')
spy.mockResolvedValue(undefined)

// Verify mock was called
expect(mockQuery).toHaveBeenCalledWith(
  'SELECT * FROM users WHERE id = ?',
  ['123']
)
expect(mockQuery).toHaveBeenCalledTimes(1)

Testing Async Code

// Async/await
it('should fetch user data', async () => {
  const user = await fetchUser('123')
  expect(user.name).toBe('John')
})

// Testing promises that reject
it('should handle fetch errors', async () => {
  await expect(fetchUser('invalid'))
    .rejects
    .toThrow('User not found')
})

// Testing timers
it('should debounce calls', () => {
  jest.useFakeTimers()
  
  const callback = jest.fn()
  const debounced = debounce(callback, 100)
  
  debounced()
  debounced()
  debounced()
  
  expect(callback).not.toHaveBeenCalled()
  
  jest.advanceTimersByTime(100)
  
  expect(callback).toHaveBeenCalledTimes(1)
})

Testing Error Handling

it('should handle network errors gracefully', async () => {
  mockFetch.mockRejectedValue(new Error('Network error'))
  
  const result = await fetchDataWithRetry('/api/data')
  
  expect(result).toEqual({ error: 'Failed to fetch data' })
  expect(mockFetch).toHaveBeenCalledTimes(3) // Retried 3 times
})

React Testing Library Patterns

Component Testing

import { render, screen, fireEvent, waitFor } from '@testing-library/react'

describe('LoginForm', () => {
  it('should submit form with credentials', async () => {
    const onSubmit = jest.fn()
    render(<LoginForm onSubmit={onSubmit} />)
    
    // Find elements by accessible roles/text
    const emailInput = screen.getByLabelText(/email/i)
    const passwordInput = screen.getByLabelText(/password/i)
    const submitButton = screen.getByRole('button', { name: /sign in/i })
    
    // Interact with form
    fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
    fireEvent.change(passwordInput, { target: { value: 'password123' } })
    fireEvent.click(submitButton)
    
    // Assert
    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123'
      })
    })
  })

  it('should display validation errors', async () => {
    render(<LoginForm onSubmit={jest.fn()} />)
    
    fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
    
    expect(await screen.findByText(/email is required/i)).toBeInTheDocument()
    expect(await screen.findByText(/password is required/i)).toBeInTheDocument()
  })
})

Testing Custom Hooks

import { renderHook, act } from '@testing-library/react'

describe('useCounter', () => {
  it('should increment counter', () => {
    const { result } = renderHook(() => useCounter(0))
    
    expect(result.current.count).toBe(0)
    
    act(() => {
      result.current.increment()
    })
    
    expect(result.current.count).toBe(1)
  })

  it('should reset counter', () => {
    const { result } = renderHook(() => useCounter(5))
    
    act(() => {
      result.current.increment()
      result.current.reset()
    })
    
    expect(result.current.count).toBe(5)
  })
})

Testing with Context

const wrapper = ({ children }) => (
  <AuthProvider>
    <ThemeProvider>
      {children}
    </ThemeProvider>
  </AuthProvider>
)

it('should use auth context', () => {
  render(<UserProfile />, { wrapper })
  
  expect(screen.getByText('Logged in as John')).toBeInTheDocument()
})

Playwright E2E Patterns

Basic Page Testing

import { test, expect } from '@playwright/test'

test.describe('Authentication', () => {
  test('should login successfully', async ({ page }) => {
    await page.goto('/login')
    
    await page.fill('[data-testid="email"]', 'user@example.com')
    await page.fill('[data-testid="password"]', 'password123')
    await page.click('[data-testid="submit"]')
    
    await expect(page).toHaveURL('/dashboard')
    await expect(page.locator('[data-testid="welcome"]'))
      .toContainText('Welcome, User')
  })

  test('should show error for invalid credentials', async ({ page }) => {
    await page.goto('/login')
    
    await page.fill('[data-testid="email"]', 'wrong@example.com')
    await page.fill('[data-testid="password"]', 'wrongpassword')
    await page.click('[data-testid="submit"]')
    
    await expect(page.locator('[data-testid="error"]'))
      .toContainText('Invalid credentials')
  })
})

Page Object Model

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login')
  }

  async login(email: string, password: string) {
    await this.page.fill('[data-testid="email"]', email)
    await this.page.fill('[data-testid="password"]', password)
    await this.page.click('[data-testid="submit"]')
  }

  async getErrorMessage() {
    return this.page.locator('[data-testid="error"]').textContent()
  }
}

// tests/login.spec.ts
test('should login', async ({ page }) => {
  const loginPage = new LoginPage(page)
  await loginPage.goto()
  await loginPage.login('user@example.com', 'password123')
  
  await expect(page).toHaveURL('/dashboard')
})

API Testing with Playwright

test('should create user via API', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: {
      name: 'John',
      email: 'john@example.com'
    }
  })
  
  expect(response.ok()).toBeTruthy()
  
  const user = await response.json()
  expect(user).toMatchObject({
    id: expect.any(String),
    name: 'John',
    email: 'john@example.com'
  })
})

Visual Testing

test('should match screenshot', async ({ page }) => {
  await page.goto('/dashboard')
  
  await expect(page).toHaveScreenshot('dashboard.png', {
    maxDiffPixels: 100
  })
})

Test Data Patterns

Factory Pattern

// factories/user.factory.ts
export function createUser(overrides?: Partial<User>): User {
  return {
    id: faker.string.uuid(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
    createdAt: new Date(),
    ...overrides
  }
}

// Usage in tests
const user = createUser({ name: 'Custom Name' })

Builder Pattern

class UserBuilder {
  private user: Partial<User> = {}

  withName(name: string) {
    this.user.name = name
    return this
  }

  withEmail(email: string) {
    this.user.email = email
    return this
  }

  asAdmin() {
    this.user.role = 'admin'
    return this
  }

  build(): User {
    return {
      id: faker.string.uuid(),
      name: this.user.name ?? faker.person.fullName(),
      email: this.user.email ?? faker.internet.email(),
      role: this.user.role ?? 'user',
      createdAt: new Date()
    }
  }
}

// Usage
const adminUser = new UserBuilder().withName('Admin').asAdmin().build()

Coverage Requirements

Minimum Coverage Targets

  • 80% overall for all code
  • 100% required for:
    • Financial calculations
    • Authentication logic
    • Security-critical code
    • Core business logic

Running Coverage

# Jest
npm test -- --coverage

# Vitest
npx vitest run --coverage

# Check thresholds
npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}'

Edge Cases to Test

  • Null/undefined inputs
  • Empty arrays/strings
  • Boundary values (0, -1, MAX_INT)
  • Unicode characters
  • Very long strings
  • Concurrent operations
  • Network failures
  • Timeout scenarios
  • Permission denied errors

Anti-Patterns to Avoid

Testing Implementation Details

// BAD: Testing internal state
expect(component.state.isLoading).toBe(true)

// GOOD: Testing visible behavior
expect(screen.getByTestId('spinner')).toBeInTheDocument()

Brittle Selectors

// BAD: Fragile selectors
page.locator('.btn-primary.mt-4.px-6')

// GOOD: Semantic selectors
page.locator('[data-testid="submit-button"]')
page.getByRole('button', { name: 'Submit' })

Over-Mocking

// BAD: Mocking everything
jest.mock('./utils')
jest.mock('./helpers')
jest.mock('./constants')

// GOOD: Only mock external dependencies
jest.mock('./api-client')

Checklist

  • Tests follow AAA pattern
  • Each test has single assertion focus
  • No shared state between tests
  • Proper cleanup in afterEach
  • Meaningful test descriptions
  • Edge cases covered
  • Async code properly awaited
  • Coverage meets thresholds

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon