Testing Guidelines
Testing standards and best practices
๐ Overview
Comprehensive testing ensures code quality and prevents regressions. This guide covers testing standards, patterns, and best practices for Izri.
๐ฏ Testing Philosophy
Testing Pyramid
/\
/E2E\
/------\
/ API \
/----------\
/ Unit \
/--------------\
- Unit Tests: 70% - Fast, isolated, numerous
- Integration Tests: 20% - API/database interactions
- E2E Tests: 10% - Critical user flows
Goals
- Confidence: Catch bugs before production
- Documentation: Tests as usage examples
- Refactoring: Safe code changes
- Speed: Fast feedback loop
๐งช Test Types
Unit Tests
Purpose: Test individual functions/components in isolation
Tools: Vitest, Testing Library
Examples:
// Function test
import { describe, expect, it } from 'vitest'
import { calculatePassRate } from './utils'
describe('calculatePassRate', () => {
it('calculates correct pass rate', () => {
expect(calculatePassRate(80, 100)).toBe(80)
})
it('handles zero total tests', () => {
expect(calculatePassRate(0, 0)).toBe(0)
})
it('rounds to 2 decimal places', () => {
expect(calculatePassRate(2, 3)).toBe(66.67)
})
})
// Component test
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { ProjectCard } from './ProjectCard'
describe('ProjectCard', () => {
const mockProject = {
id: 'proj_123',
name: 'Test Project',
repository: 'https://github.com/test/repo',
language: 'TypeScript'
}
it('renders project name', () => {
render(<ProjectCard project={mockProject} />)
expect(screen.getByText('Test Project')).toBeInTheDocument()
})
it('displays repository URL', () => {
render(<ProjectCard project={mockProject} />)
expect(screen.getByText(/github.com\/test\/repo/)).toBeInTheDocument()
})
})
Integration Tests
Purpose: Test multiple units working together
Examples:
// tRPC procedure test
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
import { createCaller } from '../context'
describe('projects.createProject', () => {
beforeEach(async () => {
await setupTestDatabase()
})
afterEach(async () => {
await cleanupTestDatabase()
})
it('creates project in database', async () => {
const caller = await createCaller({ user: testUser })
const result = await caller.projects.createProject({
name: 'Test Project',
repository: 'https://github.com/test/repo',
language: 'TypeScript',
userEmail: testUser.email
})
expect(result.project.id).toBeDefined()
expect(result.project.name).toBe('Test Project')
// Verify in database
const dbProject = await db.query.projects.findFirst({
where: eq(projects.id, result.project.id)
})
expect(dbProject).toBeDefined()
})
it('throws error for duplicate repository', async () => {
const caller = await createCaller({ user: testUser })
await caller.projects.createProject({
name: 'Project 1',
repository: 'https://github.com/test/repo',
userEmail: testUser.email
})
await expect(
caller.projects.createProject({
name: 'Project 2',
repository: 'https://github.com/test/repo',
userEmail: testUser.email
})
).rejects.toThrow('Project with this repository already exists')
})
})
E2E Tests
Purpose: Test complete user flows
Tools: Playwright
Examples:
import { test, expect } from '@playwright/test'
test.describe('Project Management', () => {
test('user can create a project', async ({ page }) => {
// Navigate to login
await page.goto('/login')
// Login with GitHub (mock)
await page.click('text=Sign in with GitHub')
// Navigate to projects
await page.goto('/dashboard/projects')
// Click new project
await page.click('text=New Project')
// Fill form
await page.fill('[name="name"]', 'E2E Test Project')
await page.fill('[name="repository"]', 'https://github.com/test/e2e-repo')
await page.selectOption('[name="language"]', 'TypeScript')
// Submit
await page.click('button:has-text("Create Project")')
// Verify success
await expect(page).toHaveURL(/\/dashboard\/projects\/proj_/)
await expect(page.locator('h1')).toHaveText('E2E Test Project')
})
})
๐ Testing Patterns
Arrange-Act-Assert (AAA)
describe('MyComponent', () => {
it('does something', () => {
// Arrange: Set up test data and conditions
const mockData = { id: '123', name: 'Test' }
render(<MyComponent data={mockData} />)
// Act: Perform the action
fireEvent.click(screen.getByRole('button'))
// Assert: Verify the result
expect(screen.getByText('Success')).toBeInTheDocument()
})
})
Test Fixtures
// test/fixtures/project.ts
export const createTestProject = (overrides = {}) => ({
id: 'proj_test_123',
name: 'Test Project',
repository: 'https://github.com/test/repo',
language: 'TypeScript',
framework: 'React',
userId: 'user_test_123',
createdAt: new Date(),
updatedAt: new Date(),
...overrides
})
// Usage
describe('ProjectCard', () => {
it('renders project', () => {
const project = createTestProject({ name: 'Custom Name' })
render(<ProjectCard project={project} />)
expect(screen.getByText('Custom Name')).toBeInTheDocument()
})
})
Mocking
// Mock tRPC client
import { vi } from 'vitest'
const mockTrpc = {
projects: {
list: {
useQuery: vi.fn(() => ({
data: { projects: [createTestProject()] },
isLoading: false
}))
}
}
}
vi.mock('~/lib/trpc', () => ({
trpc: mockTrpc
}))
Test Utilities
// test/utils.tsx
import { render } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
})
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
)
}
โ Best Practices
Do's
โ Test behavior, not implementation
// โ
Good: Test what user sees
expect(screen.getByText('Project created')).toBeInTheDocument()
// โ Bad: Test implementation details
expect(component.state.isCreated).toBe(true)
โ Use descriptive test names
// โ
Good
it('displays error message when project name is empty', () => {})
// โ Bad
it('test 1', () => {})
โ Test edge cases
describe('calculatePassRate', () => {
it('handles normal case', () => {})
it('handles zero passed tests', () => {})
it('handles zero total tests', () => {})
it('handles 100% pass rate', () => {})
})
โ Keep tests independent
// Each test should work standalone
describe('Projects', () => {
beforeEach(() => {
// Reset state for each test
cleanup()
})
it('creates project', () => {
// Independent test
})
it('deletes project', () => {
// Also independent
})
})
โ Use meaningful assertions
// โ
Good: Specific assertion
expect(project.name).toBe('Expected Name')
// โ Bad: Generic assertion
expect(project).toBeTruthy()
Don'ts
โ Don't test framework code
// โ Bad: Testing React itself
it('useState returns state and setter', () => {
const [state, setState] = useState(0)
expect(typeof setState).toBe('function')
})
โ Don't use brittle selectors
// โ Bad: Fragile selector
screen.getByClassName('btn-primary')
// โ
Good: Semantic selector
screen.getByRole('button', { name: 'Create Project' })
โ Don't have hidden dependencies
// โ Bad: Depends on previous test
let projectId
it('creates project', () => {
projectId = createProject()
})
it('deletes project', () => {
deleteProject(projectId) // Breaks if first test skipped
})
// โ
Good: Independent
it('deletes project', () => {
const projectId = createProject()
deleteProject(projectId)
})
๐ Coverage Goals
Target Coverage
Overall: 80%+
Unit Tests: 90%+
Integration Tests: 70%+
Critical Paths: 100%
Check Coverage
# Run tests with coverage
pnpm test --coverage
# View HTML report
open coverage/index.html
Coverage Configuration
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'test/',
'**/*.test.ts',
'**/*.spec.ts',
'**/types.ts'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
})
๐ Running Tests
Commands
# Run all tests
pnpm test
# Run specific file
pnpm test projects.test.ts
# Run in watch mode
pnpm test --watch
# Run with UI
pnpm test --ui
# Run coverage
pnpm test --coverage
# Run E2E tests
pnpm test:e2e
CI Pipeline
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
- run: pnpm install
- run: pnpm type:check
- run: pnpm lint
- run: pnpm test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
๐ Related Documentation
- Code Style: Code style guide
- Testing Backend: Backend testing
- Testing Frontend: Frontend testing
Contributing section complete! Continue with API Reference โ