Testing
Comprehensive testing strategies including unit, integration, and end-to-end testing
Testing Strategy
WI-CARPOOL employs a comprehensive testing strategy to ensure reliability, performance, and user satisfaction.
๐งช Unit Testing
Component-level testing with Jest and React Testing Library.
๐ Integration Testing
API integration and component interaction testing.
๐ญ E2E Testing
End-to-end user journey testing with Playwright.
๐ Coverage Reports
Code coverage tracking and reporting for quality assurance.
Unit Testing
Component Testing Example
// __tests__/components/RideCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { vi } from 'vitest'
import { RideCard } from '@/components/RideCard'
const mockRide = {
id: '1',
from: 'New York',
to: 'Boston',
date: '2024-01-15',
price: 25,
availableSeats: 3,
driver: {
name: 'John Doe',
rating: 4.8,
avatar: '/avatar.jpg'
}
}
describe('RideCard', () => {
const mockOnBook = vi.fn()
beforeEach(() => {
mockOnBook.mockClear()
})
it('renders ride information correctly', () => {
render(<RideCard ride={mockRide} onBook={mockOnBook} />)
expect(screen.getByText('New York โ Boston')).toBeInTheDocument()
expect(screen.getByText('$25')).toBeInTheDocument()
expect(screen.getByText('3 seats available')).toBeInTheDocument()
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
it('calls onBook when book button is clicked', () => {
render(<RideCard ride={mockRide} onBook={mockOnBook} />)
const bookButton = screen.getByRole('button', { name: /book ride/i })
fireEvent.click(bookButton)
expect(mockOnBook).toHaveBeenCalledWith(mockRide.id)
})
it('disables book button when no seats available', () => {
const fullRide = { ...mockRide, availableSeats: 0 }
render(<RideCard ride={fullRide} onBook={mockOnBook} />)
const bookButton = screen.getByRole('button', { name: /fully booked/i })
expect(bookButton).toBeDisabled()
})
})
Hook Testing
// __tests__/hooks/useRideSearch.test.tsx
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { vi } from 'vitest'
import { useRideSearch } from '@/hooks/useRideSearch'
import * as rideService from '@/api/services/rideService'
// Mock the ride service
vi.mock('@/api/services/rideService')
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
describe('useRideSearch', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns loading state initially', () => {
vi.mocked(rideService.searchRides).mockResolvedValue([])
const { result } = renderHook(
() => useRideSearch({ from: 'NYC', to: 'Boston', date: '2024-01-15' }),
{ wrapper: createWrapper() }
)
expect(result.current.isLoading).toBe(true)
expect(result.current.data).toBeUndefined()
})
it('returns search results on success', async () => {
const mockRides = [mockRide]
vi.mocked(rideService.searchRides).mockResolvedValue(mockRides)
const { result } = renderHook(
() => useRideSearch({ from: 'NYC', to: 'Boston', date: '2024-01-15' }),
{ wrapper: createWrapper() }
)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockRides)
expect(result.current.error).toBeNull()
})
it('handles search errors', async () => {
const error = new Error('Search failed')
vi.mocked(rideService.searchRides).mockRejectedValue(error)
const { result } = renderHook(
() => useRideSearch({ from: 'NYC', to: 'Boston', date: '2024-01-15' }),
{ wrapper: createWrapper() }
)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.error).toBeTruthy()
expect(result.current.data).toBeUndefined()
})
})
Integration Testing
API Integration Test
// __tests__/integration/ride-booking.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { App } from '@/App'
// Mock service worker for API mocking
const server = setupServer(
rest.get('/api/rides/search', (req, res, ctx) => {
return res(
ctx.json([
{
id: '1',
from: 'New York',
to: 'Boston',
date: '2024-01-15',
price: 25,
availableSeats: 3,
}
])
)
}),
rest.post('/api/rides/:rideId/book', (req, res, ctx) => {
return res(
ctx.json({
bookingId: 'booking-123',
status: 'confirmed'
})
)
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('Ride Booking Flow', () => {
it('completes full booking flow', async () => {
render(<App />)
// Navigate to search
fireEvent.click(screen.getByText(/find rides/i))
// Fill search form
fireEvent.change(screen.getByLabelText(/from/i), {
target: { value: 'New York' }
})
fireEvent.change(screen.getByLabelText(/to/i), {
target: { value: 'Boston' }
})
// Submit search
fireEvent.click(screen.getByRole('button', { name: /search/i }))
// Wait for results
await waitFor(() => {
expect(screen.getByText('New York โ Boston')).toBeInTheDocument()
})
// Book the ride
fireEvent.click(screen.getByRole('button', { name: /book ride/i }))
// Confirm booking
await waitFor(() => {
expect(screen.getByText(/booking confirmed/i)).toBeInTheDocument()
})
})
it('handles booking failure gracefully', async () => {
server.use(
rest.post('/api/rides/:rideId/book', (req, res, ctx) => {
return res(ctx.status(400), ctx.json({ error: 'Ride no longer available' }))
})
)
render(<App />)
// Complete search flow...
// Attempt booking
fireEvent.click(screen.getByRole('button', { name: /book ride/i }))
// Check error handling
await waitFor(() => {
expect(screen.getByText(/ride no longer available/i)).toBeInTheDocument()
})
})
})
End-to-End Testing
Playwright E2E Test
// e2e/ride-booking.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Ride Booking', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:8080')
})
test('user can search and book a ride', async ({ page }) => {
// Sign in
await page.click('text=Sign In')
await page.fill('[data-testid=email-input]', '[email protected]')
await page.fill('[data-testid=password-input]', 'password123')
await page.click('[data-testid=signin-button]')
// Wait for dashboard
await expect(page.locator('text=Dashboard')).toBeVisible()
// Navigate to search
await page.click('text=Find Rides')
// Fill search form
await page.fill('[data-testid=from-input]', 'New York')
await page.fill('[data-testid=to-input]', 'Boston')
await page.click('[data-testid=date-picker]')
await page.click('text=15') // Select 15th day
// Submit search
await page.click('[data-testid=search-button]')
// Wait for results
await expect(page.locator('[data-testid=ride-card]').first()).toBeVisible()
// Book first ride
await page.click('[data-testid=book-button]').first()
// Confirm booking in modal
await expect(page.locator('[data-testid=booking-modal]')).toBeVisible()
await page.click('[data-testid=confirm-booking]')
// Verify success
await expect(page.locator('text=Booking Confirmed')).toBeVisible()
// Check booking appears in user's trips
await page.click('text=My Trips')
await expect(page.locator('text=New York โ Boston')).toBeVisible()
})
test('user receives real-time notifications', async ({ page, context }) => {
// Enable notifications
await context.grantPermissions(['notifications'])
// Sign in and book a ride...
// Simulate driver update
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('driver-update', {
detail: { message: 'Driver is 5 minutes away' }
}))
})
// Check notification appeared
await expect(page.locator('[data-testid=notification]')).toBeVisible()
await expect(page.locator('text=Driver is 5 minutes away')).toBeVisible()
})
test('works offline', async ({ page, context }) => {
// Sign in and navigate to a page
await page.goto('/dashboard')
// Go offline
await context.setOffline(true)
// Try to navigate
await page.click('text=Profile')
// Should show offline indicator but still work
await expect(page.locator('[data-testid=offline-indicator]')).toBeVisible()
await expect(page.locator('text=Profile')).toBeVisible()
// Go back online
await context.setOffline(false)
// Offline indicator should disappear
await expect(page.locator('[data-testid=offline-indicator]')).not.toBeVisible()
})
})
Playwright Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:8080',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:8080',
reuseExistingServer: !process.env.CI,
},
})
Test Configuration
Vitest Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react-swc'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/main.tsx',
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Test Setup File
// src/test/setup.ts
import '@testing-library/jest-dom'
import { vi } from 'vitest'
// Mock IntersectionObserver
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock geolocation
global.navigator.geolocation = {
getCurrentPosition: vi.fn(),
watchPosition: vi.fn(),
clearWatch: vi.fn(),
}
// Mock localStorage
Object.defineProperty(window, 'localStorage', {
value: {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
},
})
Testing Best Practices
Unit Testing
- Test components in isolation with proper mocking
- Focus on testing behavior, not implementation details
- Use descriptive test names and organize with describe blocks
- Maintain high code coverage (aim for 80%+)
Integration Testing
- Test complete user workflows and API interactions
- Use mock service workers for consistent API responses
- Test error scenarios and edge cases
- Verify data flow between components
E2E Testing
- Test critical user journeys from start to finish
- Include tests for different devices and browsers
- Test offline functionality and PWA features
- Use page object patterns for maintainable tests
General Guidelines
- Run tests in CI/CD pipeline with quality gates
- Use data-testid attributes for reliable element selection
- Keep tests independent and deterministic
- Regular test maintenance and refactoring