| name | testing-strategist |
| description | Design testing strategies for AI and Web3 applications. Create test cases, mock data, and CI configurations. Use when writing tests, setting up test infrastructure, or when the user mentions testing, test coverage, unit test, integration test, or e2e. |
Testing Strategist (ๆต่ฏ็ญ็ฅๅธ)
Goal: ไธบ AI + Web3 ๅบ็จ่ฎพ่ฎก้ซๆ็ๆต่ฏ็ญ็ฅ๏ผ็กฎไฟไปฃ็ ่ดจ้ๅ็ณป็ป็จณๅฎๆงใ
OPC ๅๅ: ๆต่ฏ่ฆ็ฒพๅ๏ผไธ่ฟฝๆฑ 100% ่ฆ็็๏ผ่็ฆๆ ธๅฟ่ทฏๅพใ
Testing Philosophy
ๆต่ฏ้ๅญๅก (OPC ็):
โโโโโโโโโโโ
โ E2E โ โ ๆๅฐ (ๅ
ณ้ฎ็จๆทๆต็จ)
โโดโโโโโโโโโโดโ
โโโโโโโโโโโโโโโโโ
โ Integration โ โ ้้ (API/DB)
โโโดโโโโโโโโโโโโโโโโดโโ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ Unit Tests โ โ ๆๅค (็บฏๅฝๆฐ)
โโโดโโโโโโโโโโโโโโโโโโโโโโโโดโโ
1. Unit Testing
Pure Functions (ไผๅ
ๆต่ฏ)
import { describe, it, expect } from 'vitest'
import { truncateAddress, formatTokenAmount } from './format'
describe('truncateAddress', () => {
it('truncates correctly', () => {
expect(truncateAddress('0x1234567890abcdef1234567890abcdef12345678'))
.toBe('0x1234...5678')
})
it('handles short addresses', () => {
expect(truncateAddress('0x1234')).toBe('0x1234')
})
it('handles empty input', () => {
expect(truncateAddress('')).toBe('')
})
})
describe('formatTokenAmount', () => {
it('formats large numbers with decimals', () => {
expect(formatTokenAmount(1500000000000000000n, 18)).toBe('1.5')
})
it('handles zero', () => {
expect(formatTokenAmount(0n, 18)).toBe('0')
})
})
Testing Hooks
import { renderHook, act } from '@testing-library/react'
import { useWallet } from './useWallet'
describe('useWallet', () => {
it('starts disconnected', () => {
const { result } = renderHook(() => useWallet())
expect(result.current.status).toBe('disconnected')
})
it('connects successfully', async () => {
const { result } = renderHook(() => useWallet())
await act(async () => {
await result.current.connect()
})
expect(result.current.status).toBe('connected')
})
})
2. AI Testing Strategies
Response Structure Testing
import { describe, it, expect } from 'vitest'
import { analyzeText } from './analyzer'
import { AnalysisSchema } from './schemas'
describe('analyzeText', () => {
it('returns valid schema', async () => {
const result = await analyzeText('Test input')
expect(() => AnalysisSchema.parse(result)).not.toThrow()
})
it('handles empty input gracefully', async () => {
const result = await analyzeText('')
expect(result.error).toBe('EMPTY_INPUT')
})
})
AI Response Mocking
export const mockAIResponse = (response: unknown) => {
vi.mock('ai', () => ({
generateObject: vi.fn().mockResolvedValue({ object: response }),
generateText: vi.fn().mockResolvedValue({ text: JSON.stringify(response) }),
}))
}
describe('ChatService', () => {
beforeEach(() => {
mockAIResponse({
summary: 'Test summary',
sentiment: 'positive',
})
})
it('processes AI response correctly', async () => {
const result = await chatService.analyze('input')
expect(result.summary).toBe('Test summary')
})
})
Snapshot Testing for Prompts
import { buildAnalyzerPrompt } from './analyzer'
describe('Analyzer Prompt', () => {
it('generates consistent prompt', () => {
const prompt = buildAnalyzerPrompt({
input: 'Test data',
context: 'Financial',
})
expect(prompt).toMatchSnapshot()
})
})
Deterministic Testing
describe('AI Consistency', () => {
it('produces consistent output for same input', async () => {
const input = 'Analyze this market data'
const results = await Promise.all([
analyzeWithSeed(input, 42),
analyzeWithSeed(input, 42),
])
expect(results[0]).toEqual(results[1])
})
})
3. Web3 Testing Strategies
Contract Interaction Mocking
import { vi } from 'vitest'
export const mockUseAccount = (overrides = {}) => {
return {
address: '0x1234567890abcdef1234567890abcdef12345678',
isConnected: true,
chainId: 1,
...overrides,
}
}
export const mockUseWriteContract = () => ({
writeContract: vi.fn(),
isPending: false,
error: null,
})
Transaction Flow Testing
describe('useTransfer', () => {
it('handles successful transfer', async () => {
const { result } = renderHook(() => useTransfer())
await act(async () => {
await result.current.transfer('0x...', 1000000000000000000n)
})
expect(result.current.status).toBe('confirmed')
expect(result.current.hash).toBeDefined()
})
it('handles insufficient balance', async () => {
mockBalance(0n)
const { result } = renderHook(() => useTransfer())
await act(async () => {
await result.current.transfer('0x...', 1000000000000000000n)
})
expect(result.current.error?.code).toBe('INSUFFICIENT_BALANCE')
})
it('handles network switch during tx', async () => {
const { result } = renderHook(() => useTransfer())
act(() => { result.current.transfer('0x...', 1n) })
act(() => { mockChainSwitch(137) })
expect(result.current.error?.code).toBe('NETWORK_CHANGED')
})
})
BigInt Edge Cases
describe('BigInt Handling', () => {
it('handles max uint256', () => {
const maxUint256 = 2n ** 256n - 1n
expect(formatTokenAmount(maxUint256, 18)).not.toThrow()
})
it('handles precision correctly', () => {
expect(formatTokenAmount(1n, 18)).toBe('0.000000000000000001')
})
})
4. Integration Testing
API Integration
import { describe, it, expect } from 'vitest'
describe('Chat API', () => {
it('POST /api/chat returns streamed response', async () => {
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: 'Hello' }),
})
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toContain('text/event-stream')
})
it('handles rate limiting', async () => {
const responses = await Promise.all(
Array(10).fill(null).map(() =>
fetch('/api/chat', { method: 'POST', body: '{}' })
)
)
const rateLimited = responses.filter(r => r.status === 429)
expect(rateLimited.length).toBeGreaterThan(0)
})
})
Database Integration
describe('User Repository', () => {
beforeEach(async () => {
await db.user.deleteMany()
})
it('creates and retrieves user', async () => {
const user = await userRepo.create({
address: '0x1234...',
email: 'test@example.com',
})
const found = await userRepo.findByAddress('0x1234...')
expect(found).toEqual(user)
})
})
5. E2E Testing
Critical User Flows
import { test, expect } from '@playwright/test'
test('complete chat flow', async ({ page }) => {
await page.goto('/')
await page.click('[data-testid="connect-wallet"]')
await page.click('[data-testid="metamask"]')
await page.fill('[data-testid="chat-input"]', 'Hello AI')
await page.click('[data-testid="send-button"]')
await expect(page.locator('[data-testid="ai-response"]')).toBeVisible({
timeout: 30000,
})
const response = await page.textContent('[data-testid="ai-response"]')
expect(response?.length).toBeGreaterThan(0)
})
Web3 Flow
test('complete transaction flow', async ({ page }) => {
await page.goto('/swap')
await page.fill('[data-testid="amount-input"]', '0.1')
await expect(page.locator('[data-testid="gas-estimate"]')).toBeVisible()
await page.click('[data-testid="swap-button"]')
await expect(page.locator('[data-testid="tx-pending"]')).toBeVisible()
await expect(page.locator('[data-testid="tx-confirmed"]')).toBeVisible({
timeout: 60000,
})
})
6. Test Data & Mocks
Mock Data Factory
import { faker } from '@faker-js/faker'
export const createUser = (overrides = {}) => ({
id: faker.string.uuid(),
address: `0x${faker.string.hexadecimal({ length: 40 })}`,
email: faker.internet.email(),
createdAt: faker.date.past(),
...overrides,
})
export const createTransaction = (overrides = {}) => ({
hash: `0x${faker.string.hexadecimal({ length: 64 })}`,
from: `0x${faker.string.hexadecimal({ length: 40 })}`,
to: `0x${faker.string.hexadecimal({ length: 40 })}`,
value: BigInt(faker.number.int({ min: 1, max: 1000000 })) * 10n ** 18n,
status: 'confirmed',
...overrides,
})
AI Response Fixtures
export const AI_RESPONSES = {
ANALYSIS_SUCCESS: {
summary: 'Market shows bullish trends',
sentiment: 'positive',
confidence: 0.87,
},
ANALYSIS_UNCERTAIN: {
summary: 'Insufficient data',
sentiment: 'neutral',
confidence: 0.45,
},
ERROR_RATE_LIMITED: {
ok: false,
error: { code: 'RATE_LIMITED', retryAfter: 60 },
},
}
7. CI Configuration
GitHub Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm test:unit
- run: pnpm test:integration
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm exec playwright install
- run: pnpm test:e2e
Quick Reference
ๆต่ฏๅฝไปค
pnpm test
pnpm test:unit
pnpm test:integration
pnpm test:e2e
pnpm test:coverage
pnpm test:watch
่ฆ็็็ฎๆ (OPC)
| ๅฑ็บง | ็ฎๆ | ็็ฑ |
|---|
| ๅทฅๅ
ทๅฝๆฐ | 90%+ | ็บฏๅฝๆฐ๏ผๅฎนๆๆต่ฏ |
| Hooks | 70%+ | ๆ ธๅฟ้ป่พ |
| API Routes | 80%+ | ๅ
ณ้ฎๅ
ฅๅฃ |
| ็ปไปถ | 50%+ | ่ง่งๆต่ฏๆๆฌ้ซ |
| E2E | ๆ ธๅฟๆต็จ | ไธ่ฟฝๆฑ่ฆ็็ |
ๆจ่ๅบ
vitest - ๅๅ
/้ๆๆต่ฏ
@testing-library/react - React ็ปไปถๆต่ฏ
playwright - E2E ๆต่ฏ
msw - API mocking
@faker-js/faker - ๆต่ฏๆฐๆฎ็ๆ