Docs /contributing/testing-guidelines

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


Contributing section complete! Continue with API Reference โ†’

Reading this with an agent? /docs/contributing/testing-guidelines.md serves the raw markdown.

All docs