Testing Philosophy
Pulse Content follows a test-driven development (TDD) approach, especially for bug fixes:
Bug Fixing Workflow (Critical)When a bug is reported:
- Don’t start by trying to fix it - First write a test that reproduces the bug
- Verify the test fails - Confirming the bug exists and is captured
- Fix the bug - Implement the solution
- Verify the test passes - Ensures the fix works
- Only merge/commit when tests pass
This ensures bugs are properly understood before fixing and prevents regressions.
Testing Stack
| Tool | Purpose |
|---|
| Vitest | Unit and integration testing framework (Vite-native) |
| Testing Library | React component testing utilities |
| jsdom | DOM implementation for Node.js |
| MSW (Mock Service Worker) | API mocking for tests |
| @testing-library/user-event | User interaction simulation |
Running Tests
# Run tests in watch mode (interactive)
npm test
# Tests re-run automatically on file changes
# Great for TDD workflow
Test Structure
Unit Tests
Test individual functions and components in isolation:
// src/utils/formatDate.test.ts
import { describe, it, expect } from 'vitest'
import { formatDate } from './formatDate'
describe('formatDate', () => {
it('formats date correctly', () => {
const date = new Date('2024-03-15')
expect(formatDate(date)).toBe('March 15, 2024')
})
it('handles invalid dates', () => {
expect(formatDate(null)).toBe('Invalid date')
})
})
Component Tests
Test React components with user interactions:
// src/components/EpisodeCard.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { EpisodeCard } from './EpisodeCard'
describe('EpisodeCard', () => {
it('renders episode information', () => {
const episode = {
id: '1',
episodeNumber: 348,
title: 'Test Episode',
guest: 'John Doe'
}
render(<EpisodeCard episode={episode} />)
expect(screen.getByText('Episode 348')).toBeInTheDocument()
expect(screen.getByText('Test Episode')).toBeInTheDocument()
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
it('calls onClick when clicked', async () => {
const onClick = vi.fn()
const episode = { id: '1', episodeNumber: 348 }
render(<EpisodeCard episode={episode} onClick={onClick} />)
await userEvent.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalledWith('1')
})
})
API Tests
Test backend functions with mocked dependencies:
// functions/api/episodes/[id].test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { onRequestGet } from './[id]'
import * as sanity from '@/services/sanity'
vi.mock('@/services/sanity')
describe('GET /api/episodes/:id', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns episode data', async () => {
const mockEpisode = {
id: '1',
episodeNumber: 348,
title: 'Test Episode'
}
vi.spyOn(sanity, 'getEpisode').mockResolvedValue(mockEpisode)
const request = new Request('http://localhost/api/episodes/1')
const context = { params: { id: '1' } }
const response = await onRequestGet({ request, ...context })
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toEqual(mockEpisode)
expect(sanity.getEpisode).toHaveBeenCalledWith('1')
})
it('returns 404 for non-existent episode', async () => {
vi.spyOn(sanity, 'getEpisode').mockResolvedValue(null)
const request = new Request('http://localhost/api/episodes/999')
const context = { params: { id: '999' } }
const response = await onRequestGet({ request, ...context })
expect(response.status).toBe(404)
})
})
Mocking External Services
Mock Service Worker (MSW)
Mock HTTP requests for realistic testing:
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// Mock Sanity API
http.get('https://api.sanity.io/v1/data/query/:dataset', () => {
return HttpResponse.json({
result: [
{ _id: '1', episodeNumber: 348, title: 'Test Episode' }
]
})
}),
// Mock Kie.ai image generation
http.post('https://api.kie.ai/api/v1/jobs/createTask', () => {
return HttpResponse.json({
taskId: 'mock-task-123',
status: 'pending'
})
})
]
// src/test/setup.ts
import { setupServer } from 'msw/node'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { handlers } from './mocks/handlers'
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
Mocking Modules
Mock entire modules for unit testing:
import { vi } from 'vitest'
// Mock Sanity client
vi.mock('@/services/sanity', () => ({
getEpisode: vi.fn(),
createEpisode: vi.fn(),
updateEpisode: vi.fn()
}))
// Mock Pinecone client
vi.mock('@/services/pinecone', () => ({
queryDesigns: vi.fn().mockResolvedValue([])
}))
Test Configuration
vitest.config.ts
export default defineConfig({
test: {
globals: true, // Enable global test APIs
environment: 'jsdom', // DOM environment for React tests
setupFiles: ['./src/test/setup.ts'], // Setup file
include: [
'src/**/*.{test,spec}.{ts,tsx}',
'functions/**/*.{test,spec}.{ts,tsx}'
],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/']
}
}
})
Testing Best Practices
Test Behavior, Not Implementation
Test what the component does, not how it does it. Avoid testing internal state.
Write Descriptive Test Names
Use clear, descriptive names: it('displays error when API fails')
Follow AAA Pattern
Arrange (setup), Act (execute), Assert (verify)
Keep Tests Independent
Each test should run in isolation without depending on others
Mock External Dependencies
Mock APIs, databases, and external services for reliable tests
Test Edge Cases
Test error states, empty states, loading states, and boundary conditions
Testing Checklist
Before submitting a PR:
Type checking passes
Fix any TypeScript errors New features have tests
Write tests for new functionality before implementing (TDD)
Bug fixes have regression tests
Write a test that reproduces the bug first, then fix it
CI/CD Testing
Tests run automatically on every push via GitHub Actions:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:run
- name: Type check
run: npm run typecheck
deploy:
needs: test # Only deploys if tests pass
if: github.ref == 'refs/heads/main'
Deployment to Cloudflare Pages only occurs if all tests pass.
Common Testing Patterns
Testing Hooks
import { renderHook, waitFor } from '@testing-library/react'
import { useEpisode } from './useEpisode'
it('fetches episode data', async () => {
const { result } = renderHook(() => useEpisode('1'))
expect(result.current.loading).toBe(true)
await waitFor(() => {
expect(result.current.loading).toBe(false)
expect(result.current.episode).toBeDefined()
})
})
Testing Async Operations
it('generates PRF document', async () => {
const { result } = renderHook(() => useGeneratePRF())
act(() => {
result.current.generate('transcript text')
})
await waitFor(() => {
expect(result.current.status).toBe('complete')
expect(result.current.prf).toBeDefined()
}, { timeout: 10000 })
})
Testing Error States
it('displays error on API failure', async () => {
// Mock API failure
vi.spyOn(sanity, 'getEpisode').mockRejectedValue(
new Error('Network error')
)
render(<EpisodeDetail id="1" />)
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument()
})
})
Next Steps
Contributing Guidelines
Learn how to contribute code
Bug Workflow
Follow the bug-fixing process
CI/CD Pipeline
Understand automated testing and deployment
Architecture
Review system architecture