Testing Strategy and Implementation ๐งช
Build reliable reptile care software with comprehensive testing. A guide to testing philosophy, tools, and best practices in CrittrHavens.

Testing Philosophy
Quality through automated verification.
Testing Principles
Core Values:
- Test Early, Test Often: Catch bugs before they multiply
- Automate Everything: Manual testing doesn't scale
- Test Behavior, Not Implementation: Focus on what, not how
- Fast Feedback: Tests should run quickly
- Clear Failures: Know exactly what broke
Testing Pyramid
Balanced Test Suite:
/\
/E2E\ 5% - End-to-end tests
/------\
/Integr.\ 15% - Integration tests
/----------\
/ Unit \ 80% - Unit tests
/--------------\
Coverage Goals
Target Metrics:
- Overall Coverage: 80% minimum
- Critical Paths: 100% coverage
- Business Logic: 90% coverage
- UI Components: 70% coverage
- Utilities: 95% coverage
Unit Testing with Vitest
Fast, modern testing for JavaScript.
Vitest Setup
Configuration:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
coverage: {
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
],
},
},
});
Test Setup File:
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock environment variables
process.env.VITE_SUPABASE_URL = 'http://localhost:54321';
process.env.VITE_SUPABASE_ANON_KEY = 'test-key';
Writing Unit Tests
Basic Test Structure:
import { describe, it, expect, vi } from 'vitest';
import { calculateFeedingSchedule } from '@/lib/feeding';
describe('calculateFeedingSchedule', () => {
it('should calculate weekly feeding for juveniles', () => {
const schedule = calculateFeedingSchedule({
species: 'Ball Python',
age: 6, // months
weight: 200, // grams
});
expect(schedule.frequency).toBe('weekly');
expect(schedule.preySize).toBe('small rat');
});
it('should calculate bi-weekly feeding for adults', () => {
const schedule = calculateFeedingSchedule({
species: 'Ball Python',
age: 36, // months
weight: 1500, // grams
});
expect(schedule.frequency).toBe('bi-weekly');
expect(schedule.preySize).toBe('medium rat');
});
it('should throw error for unknown species', () => {
expect(() =>
calculateFeedingSchedule({
species: 'Unknown',
age: 12,
weight: 500,
})
).toThrow('Unknown species');
});
});
Testing Hooks
Custom Hook Tests:
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useCrittrs } from '@/hooks/useCrittrs';
describe('useCrittrs', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
it('should fetch crittrs for haven', async () => {
const { result } = renderHook(
() => useCrittrs('haven-123'),
{ wrapper: createWrapper() }
);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toHaveLength(3);
expect(result.current.data[0]).toHaveProperty('name');
});
});
Component Testing
Testing React components with React Testing Library.
Component Test Setup
Basic Component Test:
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CrittrCard } from '@/components/CrittrCard';
describe('CrittrCard', () => {
const mockCrittr = {
id: '123',
name: 'Sunny',
species: 'Ball Python',
morph: 'Banana',
imageUrl: '/sunny.jpg',
};
it('should render crittr information', () => {
render(<CrittrCard {...mockCrittr} />);
expect(screen.getByText('Sunny')).toBeInTheDocument();
expect(screen.getByText('Ball Python')).toBeInTheDocument();
expect(screen.getByText('Banana')).toBeInTheDocument();
});
it('should handle click events', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<CrittrCard {...mockCrittr} onClick={handleClick} />);
await user.click(screen.getByText('Sunny'));
expect(handleClick).toHaveBeenCalledWith('123');
});
it('should show loading skeleton', () => {
render(<CrittrCard isLoading />);
expect(screen.getByTestId('crittr-skeleton')).toBeInTheDocument();
});
});
Testing Forms
Form Component Test:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CrittrForm } from '@/components/CrittrForm';
describe('CrittrForm', () => {
it('should validate required fields', async () => {
const user = userEvent.setup();
render(<CrittrForm />);
// Submit without filling fields
await user.click(screen.getByRole('button', { name: /save/i }));
// Check for error messages
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeInTheDocument();
expect(screen.getByText('Species is required')).toBeInTheDocument();
});
});
it('should submit valid form', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<CrittrForm onSubmit={handleSubmit} />);
// Fill form fields
await user.type(screen.getByLabelText('Name'), 'Sunny');
await user.selectOptions(
screen.getByLabelText('Species'),
'Ball Python'
);
// Submit form
await user.click(screen.getByRole('button', { name: /save/i }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Sunny',
species: 'Ball Python',
});
});
});
});
Testing Async Components
Async Behavior:
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CrittrList } from '@/components/CrittrList';
// Mock API
const server = setupServer(
rest.get('/api/crittrs', (req, res, ctx) => {
return res(
ctx.json([
{ id: '1', name: 'Sunny' },
{ id: '2', name: 'Luna' },
])
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('CrittrList', () => {
it('should load and display crittrs', async () => {
render(<CrittrList />);
// Check loading state
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for data
await waitFor(() => {
expect(screen.getByText('Sunny')).toBeInTheDocument();
expect(screen.getByText('Luna')).toBeInTheDocument();
});
});
it('should handle API errors', async () => {
server.use(
rest.get('/api/crittrs', (req, res, ctx) => {
return res(ctx.status(500));
})
);
render(<CrittrList />);
await waitFor(() => {
expect(screen.getByText('Failed to load crittrs')).toBeInTheDocument();
});
});
});
Integration Testing
Testing feature workflows.
Database Integration Tests
Testing with Real Database:
import { createClient } from '@supabase/supabase-js';
import { CrittrService } from '@/services/CrittrService';
describe('CrittrService Integration', () => {
let supabase;
let service;
beforeAll(() => {
supabase = createClient(
process.env.TEST_SUPABASE_URL,
process.env.TEST_SUPABASE_KEY
);
service = new CrittrService(supabase);
});
afterEach(async () => {
// Clean up test data
await supabase.from('crittrs').delete().eq('name', 'Test Crittr');
});
it('should create and retrieve crittr', async () => {
// Create
const created = await service.createCrittr({
name: 'Test Crittr',
species: 'Ball Python',
haven_id: 'test-haven',
});
expect(created.id).toBeDefined();
// Retrieve
const retrieved = await service.getCrittr(created.id);
expect(retrieved.name).toBe('Test Crittr');
expect(retrieved.species).toBe('Ball Python');
});
it('should update crittr', async () => {
const crittr = await service.createCrittr({
name: 'Test Crittr',
species: 'Ball Python',
});
const updated = await service.updateCrittr(crittr.id, {
morph: 'Pastel',
});
expect(updated.morph).toBe('Pastel');
});
});
API Integration Tests
Testing API Endpoints:
import request from 'supertest';
import { app } from '@/server';
describe('API Integration', () => {
describe('POST /api/crittrs', () => {
it('should create new crittr', async () => {
const response = await request(app)
.post('/api/crittrs')
.send({
name: 'Sunny',
species: 'Ball Python',
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Sunny');
});
it('should validate input', async () => {
const response = await request(app)
.post('/api/crittrs')
.send({ name: '' }) // Invalid
.expect(400);
expect(response.body.errors).toContain('Name is required');
});
});
});
E2E Testing with Playwright
Testing complete user journeys.
Playwright Setup
Configuration:
// playwright.config.ts
import { defineConfig } 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', use: { ...devices['iPhone 13'] } },
],
});
E2E Test Examples
User Journey Test:
import { test, expect } from '@playwright/test';
test.describe('Crittr Management', () => {
test.beforeEach(async ({ page }) => {
// Login
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
test('should add new crittr', async ({ page }) => {
// Navigate to add crittr
await page.click('text=Add Crittr');
// Fill form
await page.fill('[name="name"]', 'Sunny');
await page.selectOption('[name="species"]', 'Ball Python');
await page.fill('[name="morph"]', 'Banana');
// Upload photo
await page.setInputFiles('[type="file"]', 'test-assets/snake.jpg');
// Submit
await page.click('text=Save Crittr');
// Verify success
await expect(page).toHaveURL(/\/crittrs\/\w+/);
await expect(page.locator('h1')).toHaveText('Sunny');
});
test('should log feeding', async ({ page }) => {
// Go to crittr page
await page.goto('/crittrs/123');
// Open feeding form
await page.click('text=Log Feeding');
// Fill details
await page.selectOption('[name="prey_type"]', 'rat');
await page.selectOption('[name="prey_size"]', 'small');
await page.check('[name="accepted"]');
// Submit
await page.click('text=Log Feeding');
// Verify in list
await expect(page.locator('.feeding-log').first()).toContainText('Small rat');
});
});
Mobile E2E Tests
Mobile-Specific Tests:
test.describe('Mobile Experience', () => {
test.use({ ...devices['iPhone 13'] });
test('should navigate with bottom nav', async ({ page }) => {
await page.goto('/');
// Use bottom navigation
await page.click('[aria-label="Tasks"]');
await expect(page).toHaveURL('/tasks');
await page.click('[aria-label="Inventory"]');
await expect(page).toHaveURL('/inventory');
});
test('should handle swipe gestures', async ({ page }) => {
await page.goto('/crittrs');
// Swipe to delete
const crittrCard = page.locator('.crittr-card').first();
await crittrCard.swipe('left');
// Confirm delete
await page.click('text=Delete');
await expect(crittrCard).not.toBeVisible();
});
});
Mocking Strategies
Isolating units under test.
Mocking Modules
Module Mocks:
// Mock Supabase client
vi.mock('@/integrations/supabase/client', () => ({
supabase: {
from: vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => Promise.resolve({
data: [{ id: '1', name: 'Sunny' }],
error: null,
})),
})),
})),
auth: {
getSession: vi.fn(() => Promise.resolve({
data: { session: { user: { id: 'user-123' } } },
})),
},
},
}));
Mocking Hooks
Hook Mocks:
vi.mock('@/hooks/useAuth', () => ({
useAuth: () => ({
user: { id: 'user-123', email: 'test@example.com' },
isLoading: false,
isAuthenticated: true,
signOut: vi.fn(),
}),
}));
Mocking External Services
Service Mocks:
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'mocked' }),
})
);
// Mock timers
vi.useFakeTimers();
// Advance time
vi.advanceTimersByTime(1000);
// Mock date
vi.setSystemTime(new Date('2024-01-01'));
Test File Organization
Structuring test files.
File Structure
Test Organization:
src/
โโโ components/
โ โโโ CrittrCard.tsx
โ โโโ CrittrCard.test.tsx
โโโ hooks/
โ โโโ useCrittrs.ts
โ โโโ useCrittrs.test.ts
โโโ lib/
โ โโโ utils.ts
โ โโโ utils.test.ts
โโโ test/
โโโ setup.ts
โโโ mocks/
โโโ fixtures/
e2e/
โโโ auth.spec.ts
โโโ crittr-management.spec.ts
โโโ helpers/
Test Naming
Naming Conventions:
// Describe blocks: Component/Function name
describe('CrittrCard', () => {
// Context blocks: Specific scenarios
describe('when loading', () => {
// Test cases: Should statements
it('should show skeleton', () => {});
});
describe('when data is loaded', () => {
it('should display crittr name', () => {});
it('should handle click events', () => {});
});
});
Running and Debugging
Executing and troubleshooting tests.
Test Commands
NPM Scripts:
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch",
"test:debug": "vitest --inspect-brk --inspect",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}
}
Debugging Tests
Debug Techniques:
// Console debugging
it('should calculate correctly', () => {
const result = calculate(2, 3);
console.log('Result:', result); // Debug output
expect(result).toBe(5);
});
// Debugger statement
it('should handle complex logic', () => {
debugger; // Pause here in debug mode
const result = complexFunction();
expect(result).toBeDefined();
});
// Playwright debug
test('user flow', async ({ page }) => {
await page.pause(); // Pause for manual inspection
await page.click('button');
});
CI/CD Test Automation
Automated testing in pipelines.
GitHub Actions
Test Workflow:
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:run
- name: Run E2E tests
run: |
npx playwright install
npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
Writing Effective Tests
Test quality best practices.
Good Test Principles
FIRST Principles:
- Fast: Tests run quickly
- Independent: No test depends on another
- Repeatable: Same result every time
- Self-validating: Pass or fail clearly
- Timely: Written with or before code
Test Examples
Good Test:
it('should calculate adult feeding schedule for ball python over 3 years old', () => {
const schedule = calculateFeedingSchedule({
species: 'Ball Python',
ageMonths: 48,
weightGrams: 1500,
});
expect(schedule).toEqual({
frequency: 'bi-weekly',
preySize: 'medium rat',
quantity: 1,
});
});
Bad Test:
// Too vague
it('should work', () => {
const result = doSomething();
expect(result).toBeTruthy();
});
Test Data
Test Fixtures:
// fixtures/crittrs.ts
export const mockCrittrs = {
juvenile: {
id: '1',
name: 'Baby',
species: 'Ball Python',
ageMonths: 6,
weightGrams: 200,
},
adult: {
id: '2',
name: 'Adult',
species: 'Ball Python',
ageMonths: 48,
weightGrams: 1500,
},
};
// Use in tests
import { mockCrittrs } from '@/test/fixtures/crittrs';
it('should handle juvenile', () => {
const result = process(mockCrittrs.juvenile);
// ...
});
Common Testing Patterns
Reusable testing solutions.
Test Utilities
// test/utils.ts
export function renderWithProviders(ui, options = {}) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>,
options
);
}
Snapshot Testing
it('should match snapshot', () => {
const { container } = render(<CrittrCard {...mockCrittr} />);
expect(container).toMatchSnapshot();
});
Accessibility Testing
it('should be accessible', async () => {
const { container } = render(<CrittrForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Next Steps
Continue improving test coverage.
Testing today prevents debugging tomorrow. Especially when tracking 50 reptiles. ๐ฆ