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