Skip to main content

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)&lt;200'], // 95% of requests under 200ms
http_req_failed: ['rate&lt;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.