#5CRSE Test-Driven Development Strategy

#Overview

This document outlines the Test-Driven Development (TDD) approach for the 5CRSE platform implementation. Following TDD principles, we will write tests before implementing functionality, ensuring that our code is reliable, maintainable, and meets requirements from the outset.

#Testing Philosophy

flowchart LR
    A[Write Test] -->|Red| B[Test Fails]
    B -->|Implement Code| C[Test Passes]
    C -->|Green| D[Refactor]
    D -->|Improve| A

The TDD cycle follows the "Red-Green-Refactor" pattern:

  1. Red: Write a failing test that defines the desired behavior
  2. Green: Implement the minimum code needed to pass the test
  3. Refactor: Improve the code while maintaining test coverage

#Testing Layers

flowchart TD
    A[5CRSE Testing Strategy]
    A --> B[Unit Tests]
    A --> C[Integration Tests]
    A --> D[End-to-End Tests]
    
    B --> B1[Component Tests]
    B --> B2[Service Tests]
    B --> B3[Utility Tests]
    
    C --> C1[API Integration]
    C --> C2[Service Integration]
    C --> C3[Database Integration]
    
    D --> D1[User Flows]
    D --> D2[Payment Processing]
    D --> D3[Authentication Flows]

#Test Structure and Organization

Based on the existing project setup with Vitest, we will organize our tests as follows:

src/
├── tests/
│   ├── setup.ts                # Test setup and global configuration
│   ├── components/             # Component tests
│   │   ├── EventCard.test.tsx
│   │   ├── EventFilters.test.tsx
│   │   └── ...
│   ├── services/               # Service tests
│   │   ├── TicketmasterService.test.ts
│   │   └── ...
│   ├── endpoints/              # API endpoint tests
│   │   ├── events.test.ts
│   │   ├── bookings.test.ts
│   │   └── ...
│   ├── collections/            # Payload collection tests
│   │   ├── Events.test.ts
│   │   ├── Bookings.test.ts
│   │   └── ...
│   └── integration/            # Integration tests
│       ├── eventDiscovery.test.ts
│       ├── bookingFlow.test.ts
│       └── ...

#Component Testing Strategy

React components will be tested using React Testing Library to focus on user behavior rather than implementation details.

// Example component test: src/tests/components/EventCard.test.tsx
import { render, screen } from '@testing-library/react';
import { EventCard } from '@/components/EventDiscovery/EventCard';

describe('EventCard', () => {
  const mockEvent = {
    id: '1',
    event_name: 'Test Event',
    event_date: '2025-05-01T19:00:00',
    event_price: 150,
    images: [{ image: { url: '/test-image.jpg' } }],
    location: {
      venue: 'Test Venue',
      address: '123 Test St'
    },
    categories: [{ id: 'concert', name: 'Concert' }]
  };

  test('renders event name and details', () => {
    render(<EventCard event={mockEvent} />);
    
    expect(screen.getByText('Test Event')).toBeInTheDocument();
    expect(screen.getByText('Test Venue')).toBeInTheDocument();
    expect(screen.getByText('$150')).toBeInTheDocument();
    expect(screen.getByText('Concert')).toBeInTheDocument();
  });
  
  test('includes view details and book now links', () => {
    render(<EventCard event={mockEvent} />);
    
    const viewDetailsLink = screen.getByText('View Details');
    expect(viewDetailsLink).toBeInTheDocument();
    expect(viewDetailsLink.closest('a')).toHaveAttribute('href', '/events/1');
    
    const bookNowLink = screen.getByText('Book Now');
    expect(bookNowLink).toBeInTheDocument();
    expect(bookNowLink.closest('a')).toHaveAttribute('href', '/book/1');
  });
});

#Service Testing Strategy

Services will be tested by mocking external dependencies and verifying the correct transformations and error handling.

// Example service test: src/tests/services/TicketmasterService.test.tsx
import { vi, describe, test, expect, beforeEach } from 'vitest';
import axios from 'axios';
import { TicketmasterService } from '@/services/TicketmasterService';

// Mock axios
vi.mock('axios');

describe('TicketmasterService', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });
  
  test('searchEvents returns normalized events and pagination', async () => {
    // Mock axios response
    (axios.get as any).mockResolvedValue({
      data: {
        _embedded: {
          events: [
            {
              id: 'tm1',
              name: 'Test Concert',
              dates: {
                start: {
                  localDate: '2025-05-15',
                  localTime: '19:30:00'
                }
              },
              _embedded: {
                venues: [{
                  name: 'Test Arena',
                  address: { line1: '123 Test St' },
                  city: { name: 'New York' },
                  state: { stateCode: 'NY' }
                }]
              },
              images: [{ url: 'https://example.com/image.jpg', width: 800, height: 600, ratio: '16_9' }],
              classifications: [{ segment: { name: 'Music' }, genre: { name: 'Rock' } }]
            }
          ]
        },
        page: {
          totalElements: 1,
          totalPages: 1,
          number: 0,
          size: 20
        }
      }
    });
    
    // Call the service method
    const result = await TicketmasterService.searchEvents({ keyword: 'test' });
    
    // Verify axios was called correctly
    expect(axios.get).toHaveBeenCalledWith(expect.stringContaining('/events.json'), {
      params: expect.objectContaining({
        apikey: expect.any(String),
        keyword: 'test'
      })
    });
    
    // Verify the response is normalized correctly
    expect(result).toEqual({
      events: [
        expect.objectContaining({
          id: 'tm1',
          name: 'Test Concert',
          eventDate: expect.any(Date),
          venue: expect.objectContaining({
            name: 'Test Arena',
            address: '123 Test St'
          }),
          source: 'ticketmaster'
        })
      ],
      page: {
        totalElements: 1,
        totalPages: 1,
        number: 0,
        size: 20
      }
    });
  });
  
  test('handles empty results from Ticketmaster', async () => {
    // Mock axios response with no events
    (axios.get as any).mockResolvedValue({
      data: {
        page: {
          totalElements: 0,
          totalPages: 0,
          number: 0,
          size: 20
        }
      }
    });
    
    // Call the service method
    const result = await TicketmasterService.searchEvents({ keyword: 'nonexistent' });
    
    // Verify the response handles empty results
    expect(result).toEqual({
      events: [],
      page: {
        totalElements: 0,
        totalPages: 0,
        number: 0,
        size: 0
      }
    });
  });
  
  test('handles API errors gracefully', async () => {
    // Mock axios error
    (axios.get as any).mockRejectedValue(new Error('API Error'));
    
    // Verify the error is caught and rethrown
    await expect(TicketmasterService.searchEvents()).rejects.toThrow('API Error');
  });
});

#API Endpoint Testing

API endpoints will be tested by mocking the request and response objects and verifying correct behaviors.

// Example endpoint test: src/tests/endpoints/events.test.ts
import { vi, describe, test, expect, beforeEach } from 'vitest';
import { getEventsHandler } from '@/endpoints/api/events';
import { TicketmasterService } from '@/services/TicketmasterService';
import { getPayloadClient } from '@/payload/payloadClient';

// Mock dependencies
vi.mock('@/services/TicketmasterService');
vi.mock('@/payload/payloadClient');

describe('Events API Endpoints', () => {
  const mockReq = {
    query: {},
    body: {},
    method: ''
  };
  
  const mockRes = {
    status: vi.fn().mockReturnThis(),
    json: vi.fn(),
    send: vi.fn()
  };
  
  beforeEach(() => {
    vi.resetAllMocks();
    mockRes.status.mockReturnThis();
  });
  
  test('GET /api/events returns combined events from Payload and Ticketmaster', async () => {
    // Setup mocks
    mockReq.method = 'GET';
    mockReq.query = { keyword: 'concert' };
    
    // Mock Payload CMS events
    const mockPayload = {
      find: vi.fn().mockResolvedValue({
        docs: [
          { id: 'p1', event_name: 'Custom Event', event_date: '2025-06-01' }
        ],
        totalDocs: 1,
        totalPages: 1,
        page: 1,
        limit: 10
      })
    };
    (getPayloadClient as any).mockResolvedValue(mockPayload);
    
    // Mock Ticketmaster events
    (TicketmasterService.searchEvents as any).mockResolvedValue({
      events: [
        { id: 'tm1', name: 'Ticketmaster Event', eventDate: new Date('2025-05-15') }
      ],
      page: { totalElements: 1, totalPages: 1, number: 0, size: 20 }
    });
    
    // Call the handler
    await getEventsHandler(mockReq as any, mockRes as any);
    
    // Verify Payload and Ticketmaster were called
    expect(mockPayload.find).toHaveBeenCalledWith({
      collection: 'events',
      where: expect.any(Object)
    });
    
    expect(TicketmasterService.searchEvents).toHaveBeenCalledWith(
      expect.objectContaining({ keyword: 'concert' })
    );
    
    // Verify response
    expect(mockRes.status).toHaveBeenCalledWith(200);
    expect(mockRes.json).toHaveBeenCalledWith({
      events: expect.arrayContaining([
        expect.objectContaining({ event_name: 'Custom Event' }),
        expect.objectContaining({ name: 'Ticketmaster Event' })
      ]),
      pagination: expect.any(Object)
    });
  });
  
  test('handles errors gracefully', async () => {
    // Setup mocks
    mockReq.method = 'GET';
    
    // Mock error in Payload
    const mockError = new Error('Database error');
    (getPayloadClient as any).mockRejectedValue(mockError);
    
    // Call the handler
    await getEventsHandler(mockReq as any, mockRes as any);
    
    // Verify error response
    expect(mockRes.status).toHaveBeenCalledWith(500);
    expect(mockRes.json).toHaveBeenCalledWith(
      expect.objectContaining({ 
        error: expect.stringContaining('Error fetching events') 
      })
    );
  });
});

#Collection Testing

Payload CMS collections will be tested to ensure they have the correct fields, hooks, and behaviors.

// Example collection test: src/tests/collections/Events.test.ts
import { describe, test, expect } from 'vitest';
import { Events } from '@/collections/Events';

describe('Events Collection', () => {
  test('has the correct fields defined', () => {
    // Basic structure checks
    expect(Events).toBeDefined();
    expect(Events.slug).toBe('events');
    
    // Fields check
    const fieldNames = Events.fields.map(field => field.name);
    expect(fieldNames).toContain('event_name');
    expect(fieldNames).toContain('event_date');
    expect(fieldNames).toContain('description');
    expect(fieldNames).toContain('location');
    expect(fieldNames).toContain('event_price');
    expect(fieldNames).toContain('images');
    expect(fieldNames).toContain('ticketmaster_id');
    expect(fieldNames).toContain('status');
  });
  
  test('event_name field is configured correctly', () => {
    const eventNameField = Events.fields.find(field => field.name === 'event_name');
    expect(eventNameField).toBeDefined();
    expect(eventNameField?.type).toBe('text');
    expect(eventNameField?.required).toBe(true);
  });
  
  test('location field is a group with required subfields', () => {
    const locationField = Events.fields.find(field => field.name === 'location');
    expect(locationField).toBeDefined();
    expect(locationField?.type).toBe('group');
    
    // Check subfields
    const subfields = locationField?.fields || [];
    const subfieldNames = subfields.map(field => field.name);
    expect(subfieldNames).toContain('venue');
    expect(subfieldNames).toContain('address');
    expect(subfieldNames).toContain('city');
    expect(subfieldNames).toContain('state');
  });
  
  test('has appropriate hooks defined', () => {
    expect(Events.hooks).toBeDefined();
    expect(Events.hooks?.beforeChange).toBeDefined();
    expect(Events.hooks?.afterChange).toBeDefined();
  });
});

#Integration Testing

Integration tests will verify that components work together correctly, focusing on key user flows.

// Example integration test: src/tests/integration/eventDiscovery.test.tsx
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { vi, describe, test, expect, beforeEach } from 'vitest';
import EventDiscoveryPage from '@/app/(frontend)/events/page';
import { TicketmasterService } from '@/services/TicketmasterService';
import { getPayloadClient } from '@/payload/payloadClient';

// Mock dependencies
vi.mock('@/services/TicketmasterService');
vi.mock('@/payload/payloadClient');

describe('Event Discovery Integration', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    
    // Mock Payload events
    const mockPayload = {
      find: vi.fn().mockResolvedValue({
        docs: [
          { 
            id: 'p1', 
            event_name: 'Custom Event', 
            event_date: '2025-06-01',
            event_price: 100,
            images: [{ image: { url: '/test-image.jpg' } }],
            location: { venue: 'Test Venue' }
          }
        ],
        totalDocs: 1,
        totalPages: 1,
        page: 1,
        limit: 10
      })
    };
    (getPayloadClient as any).mockResolvedValue(mockPayload);
    
    // Mock Ticketmaster events
    (TicketmasterService.searchEvents as any).mockResolvedValue({
      events: [
        { 
          id: 'tm1', 
          name: 'Ticketmaster Event', 
          eventDate: new Date('2025-05-15'),
          venue: { name: 'TM Arena' },
          images: [{ url: '/tm-image.jpg' }]
        }
      ],
      page: { totalElements: 1, totalPages: 1, number: 0, size: 20 }
    });
  });
  
  test('renders event cards with data from both sources', async () => {
    render(<EventDiscoveryPage />);
    
    // Initially shows loading state
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    
    // Wait for content to load
    await waitFor(() => {
      expect(screen.getByText('Custom Event')).toBeInTheDocument();
      expect(screen.getByText('Ticketmaster Event')).toBeInTheDocument();
    });
    
    // Verify the venues are displayed
    expect(screen.getByText('Test Venue')).toBeInTheDocument();
    expect(screen.getByText('TM Arena')).toBeInTheDocument();
  });
  
  test('filters work correctly', async () => {
    render(<EventDiscoveryPage />);
    
    // Wait for content to load
    await waitFor(() => {
      expect(screen.getByText('Custom Event')).toBeInTheDocument();
    });
    
    // Mock the filter change
    const concertFilter = screen.getByText('Concert');
    fireEvent.click(concertFilter);
    
    // Mock filtered Payload response
    const mockPayload = getPayloadClient();
    mockPayload.find.mockResolvedValueOnce({
      docs: [], // No results for this filter
      totalDocs: 0,
      totalPages: 0,
      page: 1,
      limit: 10
    });
    
    // Mock filtered Ticketmaster response
    (TicketmasterService.searchEvents as any).mockResolvedValueOnce({
      events: [
        { 
          id: 'tm2', 
          name: 'Rock Concert', 
          eventDate: new Date('2025-07-15'),
          venue: { name: 'Stadium' }
        }
      ],
      page: { totalElements: 1, totalPages: 1, number: 0, size: 20 }
    });
    
    // Verify filter applied and new results shown
    await waitFor(() => {
      expect(screen.queryByText('Custom Event')).not.toBeInTheDocument();
      expect(screen.getByText('Rock Concert')).toBeInTheDocument();
    });
  });
});

#End-to-End Testing

End-to-end tests will use Playwright to simulate real user interactions across complete flows.

// Example E2E test: src/tests/e2e/bookingFlow.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Event Booking Flow', () => {
  test('user can browse events, select one, and complete booking', async ({ page }) => {
    // Navigate to events page
    await page.goto('/events');
    
    // Verify events are displayed
    await expect(page.locator('.event-card')).toHaveCount(expect.toBeGreaterThan(0));
    
    // Click on the first event
    await page.click('.event-card:first-child');
    
    // Verify event details page loaded
    await expect(page.locator('h1')).toBeVisible();
    
    // Click "Book Now" button
    await page.click('text=Book Now');
    
    // Select transportation option (e.g., Escalade)
    await page.click('button:has-text("Cadillac Escalade")');
    
    // Fill in booking details
    await page.fill('[placeholder="Enter pickup address"]', '123 Main St, New York');
    await page.fill('[placeholder="Enter dropoff address"]', '456 Broadway, New York');
    
    // Set passenger count
    await page.click('button:has-text("+")'); // Increase to 2 passengers
    
    // Proceed to checkout
    await page.click('button:has-text("Proceed to Checkout")');
    
    // Verify on checkout page
    await expect(page.locator('h1:has-text("Complete Your Booking")')).toBeVisible();
    
    // Fill payment information (using test card)
    await page.fill('[placeholder="Card number"]', '4242 4242 4242 4242');
    await page.fill('[placeholder="MM / YY"]', '12/29');
    await page.fill('[placeholder="CVC"]', '123');
    
    // Complete booking
    await page.click('button:has-text("Complete Booking")');
    
    // Verify booking confirmation
    await expect(page.locator('h1:has-text("Booking Confirmed")')).toBeVisible();
    await expect(page.locator('text=Thank you for your booking')).toBeVisible();
  });
});

#Test CI Integration

We will set up the CI pipeline to run tests automatically on pull requests and before merging to the main branch.

# .github/workflows/test.yml
name: Test Suite

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
      - name: Run unit and integration tests
        run: npm test
      - name: Run E2E tests
        run: npm run test:e2e
      - name: Upload coverage reports
        uses: codecov/codecov-action@v3

#Test-First Development Workflow

For each feature in our implementation plan, we will follow this workflow:

  1. Feature Breakdown: Break the feature into testable units
  2. Test Writing: Write tests for each unit
  3. Implementation: Implement the minimum code to pass tests
  4. Refactoring: Improve code while maintaining test coverage
  5. Integration Testing: Verify components work together
  6. E2E Testing: Test complete user flows
gantt
    title Test-Driven Development Workflow
    dateFormat  YYYY-MM-DD
    section Event Card Component
    Write EventCard tests              :a1, 2025-04-01, 1d
    Implement EventCard                :a2, after a1, 1d
    Refactor EventCard                 :a3, after a2, 1d
    
    section Event Filtering
    Write Filter tests                 :b1, 2025-04-03, 1d
    Implement Filters                  :b2, after b1, 2d
    Integration test with Event Cards  :b3, after b2, 1d
    
    section Event Details Page
    Write Event Details tests          :c1, 2025-04-06, 1d
    Implement Event Details            :c2, after c1, 2d
    Test with real data                :c3, after c2, 1d
    
    section Booking Flow
    Write Booking Form tests           :d1, 2025-04-10, 1d
    Implement Booking Form             :d2, after d1, 2d
    End-to-End test booking flow       :d3, after d2, 1d

#Test Coverage Goals

  • Unit Tests: Aim for 80%+ coverage of all components, services, and utilities
  • Integration Tests: Cover all major user flows and interactions
  • E2E Tests: Test critical paths that involve multiple steps (e.g., complete booking flow)

#Conclusion

By adopting this TDD approach, we ensure that the 5CRSE platform is built on a foundation of reliable, tested code. This will lead to fewer bugs, easier maintenance, and more confidence in releasing new features. The test suite will serve as living documentation of the system's behavior and requirements.