#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:
- Red: Write a failing test that defines the desired behavior
- Green: Implement the minimum code needed to pass the test
- 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:
- Feature Breakdown: Break the feature into testable units
- Test Writing: Write tests for each unit
- Implementation: Implement the minimum code to pass tests
- Refactoring: Improve code while maintaining test coverage
- Integration Testing: Verify components work together
- 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.
