testing - nself-org/nchat GitHub Wiki
Comprehensive guide for writing and running tests in the nself-chat project.
The nself-chat project uses a comprehensive testing strategy with three layers:
- Unit Tests - Test individual components, hooks, and utilities in isolation
- Integration Tests - Test API routes and component interactions
- End-to-End (E2E) Tests - Test complete user flows in a browser
- Unit/Integration: Jest + React Testing Library
- E2E: Playwright
- Coverage: Jest Coverage Reports
- CI/CD: GitHub Actions
Unit tests verify individual components, functions, and modules work correctly in isolation.
Location: src/**/__tests__/*.test.{ts,tsx}
Examples:
src/components/ui/__tests__/button.test.tsxsrc/hooks/__tests__/use-messages.test.tssrc/lib/__tests__/utils.test.ts
When to write:
- Testing UI components
- Testing custom hooks
- Testing utility functions
- Testing data transformations
Integration tests verify multiple units work together correctly.
Location: src/__tests__/integration/*.test.{ts,tsx}
Examples:
src/__tests__/integration/chat-flow.test.tsxsrc/app/api/__tests__/config.test.ts
When to write:
- Testing API routes
- Testing complex component interactions
- Testing data flow between components
- Testing authentication flows
E2E tests verify complete user workflows in a real browser.
Location: e2e/*.spec.ts
Examples:
e2e/auth.spec.tse2e/chat.spec.tse2e/message-sending.spec.ts
When to write:
- Testing critical user journeys
- Testing multi-page workflows
- Testing real browser interactions
- Testing responsive behavior
# Run all tests
pnpm test:all
# Run unit tests only
pnpm test
# Run unit tests in watch mode
pnpm test:watch
# Run unit tests with coverage
pnpm test:coverage
# Run E2E tests
pnpm test:e2e
# Run E2E tests in UI mode (interactive)
pnpm test:e2e:ui
# Run specific test file
pnpm test src/hooks/__tests__/use-messages.test.ts
# Run tests matching pattern
pnpm test --testNamePattern="should send message"# Run tests in a specific directory
pnpm test src/components/chat/__tests__/
# Update snapshots
pnpm test -u
# Run tests in parallel
pnpm test --maxWorkers=4
# Run tests with verbose output
pnpm test --verbose
# Run E2E tests in specific browser
pnpm test:e2e --project=chromium
pnpm test:e2e --project=firefox
pnpm test:e2e --project=webkit
# Run E2E tests in headed mode (see browser)
pnpm test:e2e --headed
# Run E2E tests with debugging
pnpm test:e2e --debugTests run automatically on:
- Every push to any branch
- Every pull request
- Before deployments
View test results in GitHub Actions under the "CI" workflow.
/**
* Component Test Example
*/
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '../button'
describe('Button Component', () => {
it('should render with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('should call onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should be disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>)
expect(screen.getByText('Click me')).toBeDisabled()
})
})/**
* Hook Test Example
*/
import { renderHook, waitFor } from '@testing-library/react'
import { useMessages } from '../use-messages'
describe('useMessages Hook', () => {
it('should fetch messages for a channel', async () => {
const { result } = renderHook(() => useMessages('channel-1'))
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.messages).toBeDefined()
})
})/**
* Integration Test Example
*/
import { NextRequest } from 'next/server'
import { GET } from '../api/config/route'
describe('GET /api/config', () => {
it('should return app configuration', async () => {
const request = new NextRequest('http://localhost:3000/api/config')
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toHaveProperty('branding')
expect(data).toHaveProperty('theme')
})
})/**
* E2E Test Example
*/
import { test, expect } from '@playwright/test'
test.describe('Message Sending', () => {
test('should send a message', async ({ page }) => {
await page.goto('/chat/general')
const input = page.locator('[data-testid="message-input"]')
await input.fill('Hello, world!')
await input.press('Enter')
await expect(page.locator('text=Hello, world!')).toBeVisible()
})
})# Generate coverage report
pnpm test:coverage
# Open HTML report
open coverage/lcov-report/index.htmlWe aim for:
- 80%+ overall coverage
-
90%+ coverage for critical paths:
- Authentication
- Message sending/receiving
- Channel management
- User management
High Priority:
- ✅ User authentication and authorization
- ✅ Message CRUD operations
- ✅ Channel CRUD operations
- ✅ Real-time features (typing, presence)
- ✅ File uploads
- ✅ Search functionality
Medium Priority:
- ✅ UI components
- ✅ Utility functions
- ✅ Data transformations
- ✅ Form validations
Lower Priority:
- ✅ Styling and animations
- ✅ Static pages
- ✅ Documentation pages
-
Test Behavior, Not Implementation
// ❌ Bad - Testing implementation details expect(component.state.count).toBe(5) // ✅ Good - Testing user-visible behavior expect(screen.getByText('Count: 5')).toBeInTheDocument()
-
Use Descriptive Test Names
// ❌ Bad it('works', () => {}) // ✅ Good it('should display error message when login fails', () => {})
-
Arrange-Act-Assert Pattern
it('should increment counter', () => { // Arrange render(<Counter />) // Act fireEvent.click(screen.getByText('Increment')) // Assert expect(screen.getByText('Count: 1')).toBeInTheDocument() })
-
Clean Up After Tests
afterEach(() => { jest.clearAllMocks() cleanup() })
-
Test User Interactions
it('should submit form when enter is pressed', async () => { const onSubmit = jest.fn() render(<Form onSubmit={onSubmit} />) await userEvent.type(screen.getByRole('textbox'), 'Test{Enter}') expect(onSubmit).toHaveBeenCalled() })
-
Test Accessibility
it('should have accessible label', () => { render(<Input label="Email" />) expect(screen.getByLabelText('Email')).toBeInTheDocument() })
-
Test Error States
it('should show error message for invalid input', () => { render(<Form />) fireEvent.submit(screen.getByRole('form')) expect(screen.getByText('Email is required')).toBeInTheDocument() })
-
Use renderHook
const { result } = renderHook(() => useCustomHook()) expect(result.current.value).toBe(initialValue)
-
Test Async Hooks
const { result } = renderHook(() => useAsync()) await waitFor(() => { expect(result.current.loading).toBe(false) })
-
Test Hook Updates
const { result, rerender } = renderHook(({ id }) => useData(id), { initialProps: { id: 1 } }) rerender({ id: 2 }) expect(result.current.data.id).toBe(2)
-
Wait for Elements
// ✅ Good await expect(page.locator('text=Success')).toBeVisible({ timeout: 5000 }) // ❌ Bad await page.waitForTimeout(5000)
-
Use Data Test IDs
// Component <button data-testid="submit-button">Submit</button> // Test await page.locator('[data-testid="submit-button"]').click()
-
Test Mobile Viewports
test('should work on mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) await page.goto('/chat') // Test mobile-specific behavior })
Use the provided test fixtures for consistent test data:
import { createMockMessage, mockUsers } from '@/__tests__/fixtures/messages'
import { renderWithProviders } from '@/__tests__/utils/test-helpers'
// Create test data
const message = createMockMessage({
content: 'Test message',
userId: mockUsers.alice.id,
})
// Render with providers
renderWithProviders(<MessageList messages={[message]} />)# Increase timeout
pnpm test --testTimeout=10000# Install Playwright browsers
pnpm exec playwright install --with-deps# Clear Jest cache
pnpm test --clearCache- Check environment variables
- Verify dependencies are installed
- Check for timing issues (add proper waits)
- Review CI logs for specific errors
# Add debugger statement
it('test', () => {
debugger
expect(true).toBe(true)
})
# Run with --inspect-brk
node --inspect-brk node_modules/.bin/jest --runInBand# Run in headed mode with debugger
pnpm test:e2e --debug
# Take screenshots on failure (automatic in CI)
await page.screenshot({ path: 'debug.png' })
# Pause execution
await page.pause()When adding new features:
- ✅ Write tests first (TDD approach)
- ✅ Aim for 80%+ coverage
- ✅ Test happy paths and error cases
- ✅ Add E2E tests for critical flows
- ✅ Update this guide if needed
- Check existing tests for examples
- Ask in the team chat
- Review the Testing Library docs