Testing Strategy Guide
Overviewβ
This comprehensive testing strategy ensures both toto-app and toto-bo applications meet production quality standards through systematic testing at all levels.
1. Testing Pyramidβ
Testing Levelsβ
graph TD
A[E2E Tests] --> B[Integration Tests]
B --> C[Unit Tests]
C --> D[Component Tests]
A1[User Journeys] --> A
A2[Critical Paths] --> A
A3[Cross-browser] --> A
B1[API Integration] --> B
B2[Database Integration] --> B
B3[External Services] --> B
C1[Business Logic] --> C
C2[Utility Functions] --> C
C3[Data Processing] --> C
D1[React Components] --> D
D2[UI Interactions] --> D
D3[State Management] --> D
2. Unit Testingβ
Jest Configurationβ
// jest.config.ts
import nextJest from 'next/jest';
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/test/**/*',
'!src/**/index.ts',
'!src/app/api/**/*',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{ts,tsx}',
'<rootDir>/src/**/*.{test,spec}.{ts,tsx}',
],
};
export default createJestConfig(customJestConfig);
Unit Test Examplesβ
Business Logic Testingβ
// src/lib/cases/__tests__/caseService.test.ts
import { CaseService } from '../caseService';
import { Case, CaseStatus } from '@/types/case';
describe('CaseService', () => {
let caseService: CaseService;
beforeEach(() => {
caseService = new CaseService();
});
describe('createCase', () => {
it('should create a case with valid data', async () => {
const caseData: Partial<Case> = {
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium',
status: CaseStatus.ACTIVE
};
const result = await caseService.createCase(caseData);
expect(result).toBeDefined();
expect(result.title).toBe(caseData.title);
expect(result.status).toBe(CaseStatus.ACTIVE);
});
it('should throw error for invalid data', async () => {
const invalidData = {
title: '', // Invalid: empty title
description: 'Test',
location: 'Test',
animalType: 'invalid', // Invalid animal type
urgency: 'medium'
};
await expect(caseService.createCase(invalidData))
.rejects
.toThrow('Invalid case data');
});
});
describe('updateCaseStatus', () => {
it('should update case status correctly', async () => {
const caseId = 'test-case-id';
const newStatus = CaseStatus.RESOLVED;
const result = await caseService.updateCaseStatus(caseId, newStatus);
expect(result.status).toBe(newStatus);
expect(result.updatedAt).toBeDefined();
});
it('should throw error for invalid status transition', async () => {
const caseId = 'test-case-id';
const invalidStatus = CaseStatus.ACTIVE; // Invalid transition
await expect(caseService.updateCaseStatus(caseId, invalidStatus))
.rejects
.toThrow('Invalid status transition');
});
});
});
Utility Function Testingβ
// src/utils/__tests__/validation.test.ts
import { validateEmail, validatePhone, sanitizeInput } from '../validation';
describe('Validation Utils', () => {
describe('validateEmail', () => {
it('should validate correct email addresses', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('user.name@domain.co.uk')).toBe(true);
});
it('should reject invalid email addresses', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(validateEmail('@domain.com')).toBe(false);
expect(validateEmail('user@')).toBe(false);
});
});
describe('validatePhone', () => {
it('should validate correct phone numbers', () => {
expect(validatePhone('+1234567890')).toBe(true);
expect(validatePhone('(123) 456-7890')).toBe(true);
});
it('should reject invalid phone numbers', () => {
expect(validatePhone('123')).toBe(false);
expect(validatePhone('abc-def-ghij')).toBe(false);
});
});
describe('sanitizeInput', () => {
it('should remove HTML tags', () => {
const input = '<script>alert("xss")</script>Hello World';
const result = sanitizeInput(input);
expect(result).toBe('Hello World');
});
it('should handle empty input', () => {
expect(sanitizeInput('')).toBe('');
expect(sanitizeInput(null)).toBe('');
expect(sanitizeInput(undefined)).toBe('');
});
});
});
3. Component Testingβ
React Testing Library Setupβ
// src/test/setup.ts
import '@testing-library/jest-dom';
import { configure } from '@testing-library/react';
configure({ testIdAttribute: 'data-testid' });
// Mock Firebase
jest.mock('firebase/app', () => ({
initializeApp: jest.fn(),
}));
jest.mock('firebase/firestore', () => ({
getFirestore: jest.fn(),
collection: jest.fn(),
doc: jest.fn(),
getDoc: jest.fn(),
setDoc: jest.fn(),
updateDoc: jest.fn(),
deleteDoc: jest.fn(),
query: jest.fn(),
where: jest.fn(),
orderBy: jest.fn(),
limit: jest.fn(),
getDocs: jest.fn(),
}));
// Mock Next.js router
jest.mock('next/router', () => ({
useRouter: () => ({
push: jest.fn(),
pathname: '/',
query: {},
}),
}));
Component Test Examplesβ
Case Card Componentβ
// src/components/cases/__tests__/CaseCard.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CaseCard } from '../CaseCard';
import { Case, CaseStatus } from '@/types/case';
const mockCase: Case = {
id: 'test-case-1',
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium',
status: CaseStatus.ACTIVE,
createdAt: new Date(),
updatedAt: new Date(),
guardianId: 'test-guardian',
images: ['https://example.com/image.jpg']
};
describe('CaseCard', () => {
it('should render case information correctly', () => {
render(<CaseCard case={mockCase} />);
expect(screen.getByText('Test Case')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
expect(screen.getByText('Test Location')).toBeInTheDocument();
expect(screen.getByText('dog')).toBeInTheDocument();
});
it('should display urgency badge correctly', () => {
render(<CaseCard case={mockCase} />);
const urgencyBadge = screen.getByText('medium');
expect(urgencyBadge).toHaveClass('bg-yellow-100');
});
it('should handle click events', async () => {
const onCaseClick = jest.fn();
render(<CaseCard case={mockCase} onCaseClick={onCaseClick} />);
fireEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(onCaseClick).toHaveBeenCalledWith(mockCase.id);
});
});
it('should show loading state', () => {
render(<CaseCard case={mockCase} loading={true} />);
expect(screen.getByTestId('case-card-skeleton')).toBeInTheDocument();
});
});
Form Component Testingβ
// src/components/forms/__tests__/CaseForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CaseForm } from '../CaseForm';
describe('CaseForm', () => {
it('should render form fields correctly', () => {
render(<CaseForm onSubmit={jest.fn()} />);
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
expect(screen.getByLabelText(/description/i)).toBeInTheDocument();
expect(screen.getByLabelText(/location/i)).toBeInTheDocument();
expect(screen.getByLabelText(/animal type/i)).toBeInTheDocument();
expect(screen.getByLabelText(/urgency/i)).toBeInTheDocument();
});
it('should validate required fields', async () => {
const user = userEvent.setup();
render(<CaseForm onSubmit={jest.fn()} />);
const submitButton = screen.getByRole('button', { name: /submit/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/title is required/i)).toBeInTheDocument();
expect(screen.getByText(/description is required/i)).toBeInTheDocument();
});
});
it('should submit form with valid data', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<CaseForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/title/i), 'Test Case');
await user.type(screen.getByLabelText(/description/i), 'Test description');
await user.type(screen.getByLabelText(/location/i), 'Test Location');
await user.selectOptions(screen.getByLabelText(/animal type/i), 'dog');
await user.selectOptions(screen.getByLabelText(/urgency/i), 'medium');
await user.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium'
});
});
});
});
4. Integration Testingβ
API Integration Testsβ
// src/test/integration/api.test.ts
import { createMocks } from 'node-mocks-http';
import handler from '@/pages/api/cases';
describe('/api/cases', () => {
it('should create a new case', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium'
}
});
await handler(req, res);
expect(res._getStatusCode()).toBe(201);
expect(JSON.parse(res._getData())).toMatchObject({
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium'
});
});
it('should return 400 for invalid data', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
title: '', // Invalid: empty title
description: 'Test',
location: 'Test',
animalType: 'invalid',
urgency: 'medium'
}
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toHaveProperty('error');
});
it('should get cases with pagination', async () => {
const { req, res } = createMocks({
method: 'GET',
query: {
limit: '10',
page: '1'
}
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const data = JSON.parse(res._getData());
expect(data).toHaveProperty('cases');
expect(data).toHaveProperty('pagination');
});
});
Database Integration Testsβ
// src/test/integration/database.test.ts
import { db } from '@/lib/firebase';
import { CaseService } from '@/lib/cases/caseService';
describe('Database Integration', () => {
let caseService: CaseService;
beforeEach(() => {
caseService = new CaseService();
});
afterEach(async () => {
// Clean up test data
const testCases = await db.collection('cases')
.where('title', '==', 'Test Case')
.get();
const batch = db.batch();
testCases.docs.forEach(doc => batch.delete(doc.ref));
await batch.commit();
});
it('should create and retrieve case from database', async () => {
const caseData = {
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium',
status: 'active'
};
const createdCase = await caseService.createCase(caseData);
expect(createdCase.id).toBeDefined();
const retrievedCase = await caseService.getCase(createdCase.id);
expect(retrievedCase).toMatchObject(caseData);
});
it('should update case in database', async () => {
const caseData = {
title: 'Test Case',
description: 'Test description',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium',
status: 'active'
};
const createdCase = await caseService.createCase(caseData);
const updatedCase = await caseService.updateCase(createdCase.id, {
status: 'resolved'
});
expect(updatedCase.status).toBe('resolved');
expect(updatedCase.updatedAt).toBeDefined();
});
});
5. End-to-End Testingβ
Playwright Configurationβ
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/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:4000',
trace: 'on-first-retry',
},
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:4000',
reuseExistingServer: !process.env.CI,
},
});
E2E Test Examplesβ
User Journey Testsβ
// tests/e2e/user-journey.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User Journey', () => {
test('should complete case creation flow', async ({ page }) => {
// Navigate to home page
await page.goto('/');
await expect(page).toHaveTitle(/Toto/);
// Click on create case button
await page.click('[data-testid="create-case-button"]');
await expect(page).toHaveURL('/cases/create');
// Fill out case form
await page.fill('[data-testid="case-title"]', 'Test Case');
await page.fill('[data-testid="case-description"]', 'Test description for the case');
await page.fill('[data-testid="case-location"]', 'Test Location');
await page.selectOption('[data-testid="case-animal-type"]', 'dog');
await page.selectOption('[data-testid="case-urgency"]', 'medium');
// Upload image
await page.setInputFiles('[data-testid="case-images"]', 'tests/fixtures/test-image.jpg');
// Submit form
await page.click('[data-testid="submit-case"]');
// Verify success message
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="success-message"]')).toContainText('Case created successfully');
// Verify case appears in list
await page.goto('/cases');
await expect(page.locator('[data-testid="case-list"]')).toContainText('Test Case');
});
test('should complete donation flow', async ({ page }) => {
// Navigate to a case
await page.goto('/cases');
await page.click('[data-testid="case-card"]:first-child');
// Click donate button
await page.click('[data-testid="donate-button"]');
// Fill donation form
await page.fill('[data-testid="donation-amount"]', '50');
await page.fill('[data-testid="donor-email"]', 'test@example.com');
await page.selectOption('[data-testid="payment-method"]', 'stripe');
// Submit donation
await page.click('[data-testid="submit-donation"]');
// Verify payment processing
await expect(page.locator('[data-testid="payment-form"]')).toBeVisible();
});
});
Critical Path Testsβ
// tests/e2e/critical-paths.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Critical Paths', () => {
test('should handle case search and filtering', async ({ page }) => {
await page.goto('/cases');
// Test search functionality
await page.fill('[data-testid="search-input"]', 'dog');
await page.click('[data-testid="search-button"]');
await expect(page.locator('[data-testid="case-card"]')).toContainText('dog');
// Test filtering
await page.selectOption('[data-testid="urgency-filter"]', 'high');
await expect(page.locator('[data-testid="case-card"]')).toContainText('high');
});
test('should handle user authentication', async ({ page }) => {
await page.goto('/login');
// Test login with valid credentials
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});
test('should handle error states gracefully', async ({ page }) => {
// Test network error handling
await page.route('**/api/cases', route => route.abort());
await page.goto('/cases');
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="retry-button"]')).toBeVisible();
});
});
6. Performance Testingβ
Load Testing with k6β
// tests/performance/load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 200 }, // Ramp up to 200 users
{ duration: '5m', target: 200 }, // Stay at 200 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<200'], // 95% of requests under 200ms
http_req_failed: ['rate<0.01'], // Error rate under 1%
},
};
export default function() {
// Test cases endpoint
let response = http.get('https://app.betoto.pet/api/cases');
check(response, {
'cases endpoint status is 200': (r) => r.status === 200,
'cases endpoint response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
// Test case creation
let payload = JSON.stringify({
title: 'Load Test Case',
description: 'Case created during load testing',
location: 'Test Location',
animalType: 'dog',
urgency: 'medium'
});
let params = {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token'
},
};
response = http.post('https://app.betoto.pet/api/cases', payload, params);
check(response, {
'case creation status is 201': (r) => r.status === 201,
'case creation response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Performance Test Suiteβ
// tests/performance/performance.test.ts
import { test, expect } from '@playwright/test';
test.describe('Performance Tests', () => {
test('should load home page within performance budget', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(2000); // 2 seconds max
// Check Core Web Vitals
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const vitals = {};
entries.forEach((entry) => {
vitals[entry.name] = entry.value;
});
resolve(vitals);
}).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
});
});
expect(metrics).toBeDefined();
});
test('should handle large datasets efficiently', async ({ page }) => {
await page.goto('/cases');
// Wait for cases to load
await page.waitForSelector('[data-testid="case-card"]');
// Check that pagination is working
const caseCards = await page.locator('[data-testid="case-card"]').count();
expect(caseCards).toBeLessThanOrEqual(20); // Pagination limit
// Test search performance
const searchStart = Date.now();
await page.fill('[data-testid="search-input"]', 'test');
await page.click('[data-testid="search-button"]');
await page.waitForSelector('[data-testid="case-card"]');
const searchTime = Date.now() - searchStart;
expect(searchTime).toBeLessThan(1000); // 1 second max for search
});
});
7. Security Testingβ
Security Test Suiteβ
// tests/security/security.test.ts
import { test, expect } from '@playwright/test';
test.describe('Security Tests', () => {
test('should prevent XSS attacks', async ({ page }) => {
await page.goto('/cases/create');
// Try to inject script
await page.fill('[data-testid="case-title"]', '<script>alert("xss")</script>');
await page.fill('[data-testid="case-description"]', 'Normal description');
await page.click('[data-testid="submit-case"]');
// Check that script was sanitized
const titleValue = await page.inputValue('[data-testid="case-title"]');
expect(titleValue).not.toContain('<script>');
});
test('should validate input data', async ({ page }) => {
await page.goto('/cases/create');
// Try to submit empty form
await page.click('[data-testid="submit-case"]');
// Check for validation errors
await expect(page.locator('[data-testid="title-error"]')).toBeVisible();
await expect(page.locator('[data-testid="description-error"]')).toBeVisible();
});
test('should handle authentication properly', async ({ page }) => {
// Try to access protected route without auth
await page.goto('/dashboard');
// Should redirect to login
await expect(page).toHaveURL('/login');
});
test('should prevent CSRF attacks', async ({ page }) => {
// Test that forms include CSRF tokens
await page.goto('/cases/create');
const csrfToken = await page.locator('[name="csrf-token"]').getAttribute('value');
expect(csrfToken).toBeDefined();
});
});
8. Test Automationβ
Test Scriptsβ
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testPathPattern=unit",
"test:integration": "jest --testPathPattern=integration",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:performance": "k6 run tests/performance/load-test.js",
"test:security": "jest --testPathPattern=security",
"test:all": "npm run test:unit && npm run test:integration && npm run test:e2e",
"test:ci": "npm run test:coverage && npm run test:e2e && npm run test:performance"
}
}
GitHub Actions Test Workflowβ
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, staging]
pull_request:
branches: [main, staging]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
integration-tests:
runs-on: ubuntu-latest
services:
firebase-emulator:
image: firebase/emulators
ports:
- 8080:8080
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test:integration
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npx playwright install
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
performance-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test:performance
9. Testing Checklistβ
Pre-Release Testing Checklistβ
-
Unit Tests
- All business logic tested
- Utility functions tested
- Error handling tested
- Edge cases covered
-
Component Tests
- All React components tested
- User interactions tested
- Props validation tested
- State management tested
-
Integration Tests
- API endpoints tested
- Database operations tested
- External service integration tested
- Authentication flow tested
-
E2E Tests
- Critical user journeys tested
- Cross-browser compatibility tested
- Mobile responsiveness tested
- Error scenarios tested
-
Performance Tests
- Load testing completed
- Core Web Vitals within targets
- Response times acceptable
- Memory usage optimized
-
Security Tests
- XSS prevention tested
- Input validation tested
- Authentication security tested
- CSRF protection tested
This comprehensive testing strategy ensures both toto-app and toto-bo applications meet production quality standards through systematic testing at all levels.