Claude
Skills
Sign in
Back

testing-patterns

Included with Lifetime
$97 forever

Testing patterns for unit, integration, and E2E tests. Mock patterns for Supabase, Redis, OpenAI. Use when writing tests or setting up test infrastructure.

Backend & APIs

What this skill does


# Testing Patterns

Comprehensive testing patterns including unit tests, integration tests, E2E tests, and mocking strategies for common services.

## When to Use

- Writing new features (TDD)
- Testing API endpoints
- Testing React components
- Mocking external services
- Setting up E2E tests
- Verifying user flows

## Test Types Overview

| Test Type | Scope | Speed | Examples |
|-----------|-------|-------|----------|
| Unit | Individual functions/components | Fast (< 50ms) | Pure functions, utilities, hooks |
| Integration | Multiple components/modules | Medium (< 1s) | API endpoints, database operations |
| E2E | Complete user flows | Slow (2-10s) | Login flow, checkout, search |

## Unit Testing Patterns

### Testing Pure Functions

```typescript
// src/lib/utils.ts
export function calculateProbability(yesVotes: number, noVotes: number): number {
  const total = yesVotes + noVotes
  if (total === 0) return 0.5
  return yesVotes / total
}

// src/lib/utils.test.ts
import { calculateProbability } from './utils'

describe('calculateProbability', () => {
  it('returns 0.5 for equal votes', () => {
    expect(calculateProbability(10, 10)).toBe(0.5)
  })

  it('returns 0.5 for zero votes', () => {
    expect(calculateProbability(0, 0)).toBe(0.5)
  })

  it('calculates correct probability for yes bias', () => {
    expect(calculateProbability(75, 25)).toBe(0.75)
  })

  it('calculates correct probability for no bias', () => {
    expect(calculateProbability(25, 75)).toBe(0.25)
  })

  it('handles edge case of one vote', () => {
    expect(calculateProbability(1, 0)).toBe(1)
    expect(calculateProbability(0, 1)).toBe(0)
  })
})
```

### Testing React Components

```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MarketCard } from './MarketCard'

describe('MarketCard', () => {
  const mockMarket = {
    id: '1',
    name: 'Test Market',
    description: 'Test description',
    endDate: new Date('2025-12-31')
  }

  it('renders market information', () => {
    render(<MarketCard {...mockMarket} />)

    expect(screen.getByText('Test Market')).toBeInTheDocument()
    expect(screen.getByText('Test description')).toBeInTheDocument()
  })

  it('calls onBet when yes button clicked', async () => {
    const onBet = jest.fn()
    render(<MarketCard {...mockMarket} onBet={onBet} />)

    const yesButton = screen.getByRole('button', { name: /bet yes/i })
    await userEvent.click(yesButton)

    expect(onBet).toHaveBeenCalledWith('1', 'yes')
    expect(onBet).toHaveBeenCalledTimes(1)
  })

  it('disables buttons while loading', async () => {
    const onBet = jest.fn(() => new Promise(resolve => setTimeout(resolve, 100)))
    render(<MarketCard {...mockMarket} onBet={onBet} />)

    const yesButton = screen.getByRole('button', { name: /bet yes/i })
    await userEvent.click(yesButton)

    expect(yesButton).toBeDisabled()
    expect(screen.getByRole('button', { name: /bet no/i })).toBeDisabled()

    await waitFor(() => {
      expect(yesButton).not.toBeDisabled()
    })
  })

  it('shows loading spinner during bet', async () => {
    const onBet = jest.fn(() => new Promise(resolve => setTimeout(resolve, 100)))
    render(<MarketCard {...mockMarket} onBet={onBet} />)

    await userEvent.click(screen.getByRole('button', { name: /bet yes/i }))

    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()

    await waitFor(() => {
      expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument()
    })
  })
})
```

### Testing Custom Hooks

```typescript
import { renderHook, waitFor } from '@testing-library/react'
import { useDebounce } from './useDebounce'

describe('useDebounce', () => {
  beforeEach(() => {
    jest.useFakeTimers()
  })

  afterEach(() => {
    jest.useRealTimers()
  })

  it('returns initial value immediately', () => {
    const { result } = renderHook(() => useDebounce('test', 500))
    expect(result.current).toBe('test')
  })

  it('debounces value changes', () => {
    const { result, rerender } = renderHook(
      ({ value, delay }) => useDebounce(value, delay),
      { initialProps: { value: 'initial', delay: 500 } }
    )

    expect(result.current).toBe('initial')

    // Update value
    rerender({ value: 'updated', delay: 500 })

    // Value should not change immediately
    expect(result.current).toBe('initial')

    // Fast-forward time
    jest.advanceTimersByTime(500)

    // Value should now be updated
    waitFor(() => {
      expect(result.current).toBe('updated')
    })
  })

  it('cancels pending debounce on unmount', () => {
    const { unmount } = renderHook(() => useDebounce('test', 500))

    unmount()

    // Verify no errors and cleanup happened
    jest.advanceTimersByTime(500)
  })
})
```

## Integration Testing Patterns

### Testing API Routes

```typescript
// app/api/markets/route.test.ts
import { NextRequest } from 'next/server'
import { GET, POST } from './route'

// Mock database
jest.mock('@/lib/db', () => ({
  markets: {
    findMany: jest.fn(),
    create: jest.fn()
  }
}))

import { db } from '@/lib/db'

describe('GET /api/markets', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('returns markets successfully', async () => {
    const mockMarkets = [
      { id: '1', name: 'Market 1' },
      { id: '2', name: 'Market 2' }
    ]

    ;(db.markets.findMany as jest.Mock).mockResolvedValue(mockMarkets)

    const request = new NextRequest('http://localhost/api/markets')
    const response = await GET(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data.success).toBe(true)
    expect(data.data).toEqual(mockMarkets)
  })

  it('validates pagination parameters', async () => {
    const request = new NextRequest('http://localhost/api/markets?page=0')
    const response = await GET(request)

    expect(response.status).toBe(400)
  })

  it('handles database errors', async () => {
    ;(db.markets.findMany as jest.Mock).mockRejectedValue(
      new Error('Database error')
    )

    const request = new NextRequest('http://localhost/api/markets')
    const response = await GET(request)

    expect(response.status).toBe(500)
  })
})

describe('POST /api/markets', () => {
  it('creates market with valid data', async () => {
    const newMarket = {
      name: 'New Market',
      description: 'Test description',
      category: 'politics',
      endDate: '2025-12-31T00:00:00Z'
    }

    ;(db.markets.create as jest.Mock).mockResolvedValue({
      id: '1',
      ...newMarket
    })

    const request = new NextRequest('http://localhost/api/markets', {
      method: 'POST',
      body: JSON.stringify(newMarket)
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(201)
    expect(data.success).toBe(true)
    expect(db.markets.create).toHaveBeenCalledWith({
      data: newMarket
    })
  })

  it('rejects invalid data', async () => {
    const invalidMarket = {
      name: 'ab', // Too short
      description: 'short'
    }

    const request = new NextRequest('http://localhost/api/markets', {
      method: 'POST',
      body: JSON.stringify(invalidMarket)
    })

    const response = await POST(request)

    expect(response.status).toBe(400)
    expect(db.markets.create).not.toHaveBeenCalled()
  })
})
```

## Mocking External Services

### Supabase Mock

```typescript
// __mocks__/@supabase/supabase-js.ts
export const createClient = jest.fn(() => ({
  from: jest.fn((table: string) => ({
    select: jest.fn(() => ({
      eq: jest.fn(() => Promise.resolve({
        data: [{ id: '1', name: 'Test' }],
        error: null
      })),
      single: jest.fn(() => Promise.resolve({
        data: { id: '1', name: 'Test' },
        error: null
      }))
    })),
    insert: jest.fn(() => Promise.resolve({
      data: { id: '1', name: 'New Item' },
      error: null
    })),
  

Related in Backend & APIs