Testing patterns for unit, integration, and E2E tests. Mock patterns for Supabase, Redis, OpenAI. Use when writing tests or setting up test infrastructure.
Generates comprehensive unit, integration, and E2E test patterns with mocks for Supabase, Redis, and OpenAI.
npx claudepluginhub mnthe/hardworker-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive testing patterns including unit tests, integration tests, E2E tests, and mocking strategies for common services.
| 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 |
// 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)
})
})
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()
})
})
})
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)
})
})
// 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()
})
})
// __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
})),
update: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({
data: { id: '1', name: 'Updated' },
error: null
}))
})),
delete: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({
data: null,
error: null
}))
}))
})),
auth: {
getSession: jest.fn(() => Promise.resolve({
data: { session: { user: { id: '1' } } },
error: null
})),
signInWithPassword: jest.fn(() => Promise.resolve({
data: { user: { id: '1', email: 'test@example.com' } },
error: null
})),
signOut: jest.fn(() => Promise.resolve({ error: null }))
}
}))
// __mocks__/ioredis.ts
export default class Redis {
private store = new Map<string, string>()
async get(key: string): Promise<string | null> {
return this.store.get(key) || null
}
async set(key: string, value: string): Promise<'OK'> {
this.store.set(key, value)
return 'OK'
}
async setex(key: string, seconds: number, value: string): Promise<'OK'> {
this.store.set(key, value)
// In real app, would expire after seconds
return 'OK'
}
async del(key: string): Promise<number> {
const existed = this.store.has(key)
this.store.delete(key)
return existed ? 1 : 0
}
async exists(key: string): Promise<number> {
return this.store.has(key) ? 1 : 0
}
async hset(key: string, field: string, value: string): Promise<number> {
const hash = JSON.parse(this.store.get(key) || '{}')
hash[field] = value
this.store.set(key, JSON.stringify(hash))
return 1
}
async hget(key: string, field: string): Promise<string | null> {
const hash = JSON.parse(this.store.get(key) || '{}')
return hash[field] || null
}
async quit(): Promise<'OK'> {
this.store.clear()
return 'OK'
}
}
// __mocks__/openai.ts
export class OpenAI {
embeddings = {
create: jest.fn(async ({ input }: { input: string }) => ({
data: [
{
embedding: new Array(1536).fill(0).map(() => Math.random()),
index: 0
}
],
model: 'text-embedding-ada-002',
usage: {
prompt_tokens: 10,
total_tokens: 10
}
}))
}
chat = {
completions: {
create: jest.fn(async ({ messages }: { messages: any[] }) => ({
id: 'chatcmpl-123',
object: 'chat.completion',
created: Date.now(),
model: 'gpt-4',
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock AI response'
},
finish_reason: 'stop'
}
],
usage: {
prompt_tokens: 50,
completion_tokens: 10,
total_tokens: 60
}
}))
}
}
}
export default OpenAI
// lib/search.test.ts
import { searchMarkets } from './search'
jest.mock('ioredis')
jest.mock('@supabase/supabase-js')
jest.mock('openai')
import Redis from 'ioredis'
import { createClient } from '@supabase/supabase-js'
import OpenAI from 'openai'
describe('searchMarkets', () => {
let redis: Redis
let supabase: ReturnType<typeof createClient>
let openai: OpenAI
beforeEach(() => {
redis = new Redis()
supabase = createClient('url', 'key')
openai = new OpenAI({ apiKey: 'test' })
})
it('uses vector search when Redis available', async () => {
const results = await searchMarkets('election')
expect(openai.embeddings.create).toHaveBeenCalledWith({
input: 'election',
model: 'text-embedding-ada-002'
})
expect(results.method).toBe('semantic')
})
it('falls back to full-text search when Redis fails', async () => {
// Mock Redis failure
jest.spyOn(redis, 'get').mockRejectedValue(new Error('Redis down'))
const results = await searchMarkets('election')
expect(results.method).toBe('fulltext')
})
it('uses substring search as last resort', async () => {
// Mock both Redis and Supabase failures
jest.spyOn(redis, 'get').mockRejectedValue(new Error('Redis down'))
const mockSupabase = supabase as any
mockSupabase.from().select().textSearch = jest.fn(() =>
Promise.reject(new Error('Supabase error'))
)
const results = await searchMarkets('election')
expect(results.method).toBe('substring')
})
})
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
}
})
// e2e/markets.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Markets Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('displays list of markets', async ({ page }) => {
await expect(page.locator('h1')).toContainText('Markets')
const markets = page.locator('[data-testid="market-card"]')
await expect(markets).toHaveCount(10, { timeout: 5000 })
})
test('search filters markets', async ({ page }) => {
// Navigate to markets
await page.click('a[href="/markets"]')
// Search for specific term
await page.fill('input[placeholder="Search markets"]', 'election')
// Wait for debounce and results
await page.waitForTimeout(600)
// Verify filtered results
const results = page.locator('[data-testid="market-card"]')
await expect(results).toHaveCount(5)
// Verify all results contain search term
const firstResult = results.first()
await expect(firstResult).toContainText('election', { ignoreCase: true })
})
test('pagination works correctly', async ({ page }) => {
await page.click('a[href="/markets"]')
// Check first page
let markets = page.locator('[data-testid="market-card"]')
await expect(markets).toHaveCount(20)
// Go to next page
await page.click('button[aria-label="Next page"]')
// Wait for new results
await page.waitForURL(/page=2/)
// Verify different results
markets = page.locator('[data-testid="market-card"]')
await expect(markets).toHaveCount(20)
})
})
test.describe('Market Creation', () => {
test('creates new market', async ({ page }) => {
// Login first (assuming auth flow)
await page.goto('/login')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
// Navigate to creator dashboard
await page.goto('/creator-dashboard')
// Fill market form
await page.fill('input[name="name"]', 'Test Market ' + Date.now())
await page.fill('textarea[name="description"]', 'This is a test market for E2E testing')
await page.selectOption('select[name="category"]', 'politics')
await page.fill('input[name="endDate"]', '2025-12-31T23:59')
// Submit form
await page.click('button[type="submit"]')
// Verify success
await expect(page.locator('text=Market created successfully')).toBeVisible()
// Verify redirect
await expect(page).toHaveURL(/\/markets\/test-market-/)
})
test('validates required fields', async ({ page }) => {
await page.goto('/creator-dashboard')
// Try to submit without filling fields
await page.click('button[type="submit"]')
// Verify error messages
await expect(page.locator('text=Name is required')).toBeVisible()
await expect(page.locator('text=Description is required')).toBeVisible()
})
})
test.describe('Betting Flow', () => {
test('places bet on market', async ({ page }) => {
await page.goto('/markets/test-market')
// Select outcome
await page.click('button:has-text("Yes")')
// Enter amount
await page.fill('input[name="amount"]', '10')
// Confirm bet
await page.click('button:has-text("Place Bet")')
// Verify confirmation
await expect(page.locator('text=Bet placed successfully')).toBeVisible()
// Verify updated balance
await expect(page.locator('[data-testid="balance"]')).toContainText('90')
})
})
// e2e/visual.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Visual Regression', () => {
test('markets page matches snapshot', async ({ page }) => {
await page.goto('/markets')
await expect(page).toHaveScreenshot('markets-page.png')
})
test('dark mode matches snapshot', async ({ page }) => {
await page.goto('/')
await page.click('button[aria-label="Toggle dark mode"]')
await expect(page).toHaveScreenshot('dark-mode.png')
})
})
// test/matchers.ts
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling
return {
pass,
message: () =>
pass
? `Expected ${received} not to be within range ${floor} - ${ceiling}`
: `Expected ${received} to be within range ${floor} - ${ceiling}`
}
}
})
// Usage
test('probability is within valid range', () => {
const prob = calculateProbability(75, 25)
expect(prob).toBeWithinRange(0, 1)
})
// test/factories.ts
export function createMockMarket(overrides?: Partial<Market>): Market {
return {
id: '1',
name: 'Test Market',
description: 'Test description',
category: 'politics',
createdAt: new Date(),
endDate: new Date('2025-12-31'),
...overrides
}
}
export function createMockUser(overrides?: Partial<User>): User {
return {
id: '1',
email: 'test@example.com',
name: 'Test User',
role: 'user',
...overrides
}
}
// Usage
test('renders market card', () => {
const market = createMockMarket({ name: 'Custom Market' })
render(<MarketCard {...market} />)
expect(screen.getByText('Custom Market')).toBeInTheDocument()
})
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.