This document outlines the testing strategy and infrastructure for the Partial application.
We've implemented a comprehensive testing setup to catch bugs before deployment:
- Server: Unit tests, integration tests, and API endpoint tests
- Client: Component tests, hook tests, and utility function tests
- E2E: (Future) End-to-end tests with Playwright or Cypress
- Vitest: Fast test runner with TypeScript support
- Supertest: HTTP assertion library for API testing
- @vitest/ui: Visual test runner interface
- Prisma Mock: For database mocking in tests
- Vitest: Fast test runner optimized for Next.js
- React Testing Library: Component testing utilities
- @testing-library/jest-dom: DOM matchers
- MSW (Mock Service Worker): API mocking for component tests
server/
├── src/
│ ├── __tests__/ # Test files co-located with source
│ │ ├── controllers/
│ │ ├── middleware/
│ │ └── lib/
│ └── ...
├── tests/
│ ├── integration/ # Integration tests
│ ├── fixtures/ # Test data and fixtures
│ └── setup.ts # Test setup and teardown
└── vitest.config.ts
client/
├── src/
│ ├── __tests__/ # Test files co-located with source
│ │ ├── components/
│ │ ├── hooks/
│ │ └── lib/
│ └── ...
├── tests/
│ ├── setup.ts # Test setup
│ └── fixtures/ # Test fixtures
└── vitest.config.ts
cd server
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:ui # Visual test runner
npm run test:coverage # Coverage reportcd client
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:ui # Visual test runner
npm run test:coverage # Coverage reportTest individual controller functions with mocked dependencies:
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Request, Response } from 'express';
import { getUserById } from '../controllers/userController';
describe('getUserById', () => {
it('should return user when found', async () => {
// Mock Prisma
const mockUser = { id: '1', name: 'Test User' };
vi.mock('@prisma/client', () => ({
PrismaClient: vi.fn(() => ({
user: {
findFirst: vi.fn().mockResolvedValue(mockUser),
},
})),
}));
const req = { params: { userId: '1' }, auth: { organizationId: 'org1' } } as any;
const res = { json: vi.fn(), status: vi.fn().mockReturnThis() } as any;
await getUserById(req, res);
expect(res.json).toHaveBeenCalledWith(mockUser);
});
});Test full request/response cycle:
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import app from '../index';
describe('GET /users', () => {
it('should return list of users', async () => {
const response = await request(app)
.get('/users')
.set('Cookie', 'session=valid-session')
.expect(200);
expect(response.body).toBeInstanceOf(Array);
});
});Test authentication, authorization, and other middleware:
import { describe, it, expect, vi } from 'vitest';
import { authenticate } from '../middleware/authenticate';
describe('authenticate middleware', () => {
it('should reject requests without valid session', async () => {
const req = { session: {} } as any;
const res = { status: vi.fn().mockReturnThis(), json: vi.fn() } as any;
const next = vi.fn();
await authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
});Test React components with React Testing Library:
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UserCard } from '../components/UserCard';
describe('UserCard', () => {
it('should render user name', () => {
const user = { id: '1', name: 'John Doe' };
render(<UserCard user={user} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});Test custom React hooks:
import { describe, it, expect } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useAuth } from '../hooks/useAuth';
describe('useAuth', () => {
it('should return user when authenticated', async () => {
const { result } = renderHook(() => useAuth());
await waitFor(() => {
expect(result.current.user).toBeDefined();
});
});
});Test pure utility functions:
import { describe, it, expect } from 'vitest';
import { formatDate } from '../lib/utils';
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('Jan 15, 2024');
});
});- Unit Tests: 80%+ coverage for business logic
- Integration Tests: All critical API endpoints
- Component Tests: All reusable components
- Critical Paths: 100% coverage (auth, payments, data mutations)
- Co-locate test files with source files (
__tests__folders) - Use descriptive test names:
should return error when user not found - Group related tests with
describeblocks
- Each test should be independent
- Use
beforeEach/afterEachfor setup/teardown - Mock external dependencies (database, APIs, file system)
- Use factories/fixtures for consistent test data
- Avoid hardcoding test data in multiple places
- Use realistic but minimal test data
- Test behavior, not implementation
- Use meaningful assertions with clear error messages
- Test edge cases and error conditions
- Keep tests fast (< 100ms per test when possible)
- Use mocks instead of real database/API calls in unit tests
- Run tests in parallel when possible
Use a separate test database:
// tests/setup.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL,
},
},
});
beforeAll(async () => {
// Run migrations
await prisma.$connect();
});
afterAll(async () => {
await prisma.$disconnect();
});
afterEach(async () => {
// Clean up test data
await prisma.user.deleteMany();
});Mock Prisma client for fast unit tests:
vi.mock('@prisma/client', () => ({
PrismaClient: vi.fn(() => ({
user: {
findMany: vi.fn(),
create: vi.fn(),
},
})),
}));Add to your CI pipeline:
# .github/workflows/test.yml
- name: Run server tests
run: |
cd server
npm test -- --coverage
- name: Run client tests
run: |
cd client
npm test -- --coverage- Login/logout flows
- Protected route access
- Session expiration
- Role-based access control
- Request validation
- Response format
- Error handling
- Rate limiting
- Authentication requirements
- Rendering
- User interactions
- State changes
- Error states
- Loading states
- Input validation
- Business rule enforcement
- Data transformation
- Edge cases
- Start with Critical Paths: Test authentication, data mutations, and payment flows first
- Add Tests Incrementally: Write tests for new features as you build them
- Refactor Existing Code: Add tests when fixing bugs or refactoring
- Set Coverage Thresholds: Gradually increase coverage requirements
- E2E Testing: Consider adding Playwright or Cypress for end-to-end tests