Testing
UniPulse uses Vitest as the test runner for both backend and frontend, with Testing Library for React component tests and Supertest for API endpoint tests.
Testing Stack
| Tool | Purpose | Package |
|---|---|---|
| Vitest | Test runner and assertion library | vitest |
| Testing Library | React component testing (DOM interaction) | @testing-library/react |
| Supertest | HTTP API endpoint testing | supertest |
| MSW | API mocking for frontend tests | msw (optional) |
Running Tests
# Run all tests across the monorepo
npm run test
# Run tests in watch mode (re-run on file change)
npm run test:watch
# Run tests for a specific package
npm run test --filter=api
npm run test --filter=web
npm run test --filter=shared
# Run a specific test file
npx vitest run apps/api/src/services/__tests__/post.service.test.ts
# Run with coverage
npx vitest run --coverage
Test Organization
Backend Tests
apps/api/src/
├── services/
│ ├── post.service.ts
│ └── __tests__/
│ └── post.service.test.ts
├── routes/
│ ├── post.routes.ts
│ └── __tests__/
│ └── post.routes.test.ts
└── middleware/
├── auth.ts
└── __tests__/
└── auth.test.ts
Frontend Tests
apps/web/src/
├── pages/
│ ├── posts/
│ │ ├── PostList.tsx
│ │ └── __tests__/
│ │ └── PostList.test.tsx
├── components/
│ ├── DataTable.tsx
│ └── __tests__/
│ └── DataTable.test.tsx
└── stores/
├── auth.store.ts
└── __tests__/
└── auth.store.test.ts
Shared Package Tests
packages/shared/src/
├── validators/
│ ├── post.validators.ts
│ └── __tests__/
│ └── post.validators.test.ts
What to Test
Services (Business Logic)
| Test Category | What to Verify |
|---|---|
| Happy path | Correct output for valid input |
| Workspace scoping | Queries always filter by workspaceId |
| Not found | Throws NOT_FOUND for invalid IDs |
| Permission checks | Throws FORBIDDEN for unauthorized access |
| Edge cases | Empty arrays, null fields, max length inputs |
| Error propagation | AppError is thrown, not swallowed |
// apps/api/src/services/__tests__/post.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { postService } from '../post.service';
import { prisma } from '../../lib/prisma';
vi.mock('../../lib/prisma');
describe('postService', () => {
const workspaceId = 'ws_123';
describe('getAll', () => {
it('returns posts scoped to workspace', async () => {
const mockPosts = [{ id: '1', caption: 'Test', workspaceId }];
vi.mocked(prisma.post.findMany).mockResolvedValue(mockPosts);
const result = await postService.getAll(workspaceId);
expect(prisma.post.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { workspaceId },
})
);
expect(result).toEqual(mockPosts);
});
});
describe('getById', () => {
it('throws NOT_FOUND for non-existent post', async () => {
vi.mocked(prisma.post.findFirst).mockResolvedValue(null);
await expect(postService.getById(workspaceId, 'invalid'))
.rejects.toThrow('NOT_FOUND');
});
});
});
API Routes (Integration)
| Test Category | What to Verify |
|---|---|
| Auth guard | Returns 401 without JWT |
| Role guard | Returns 403 with insufficient role |
| Validation | Returns 400 for invalid request body |
| Success response | Returns correct status code and body |
| Error response | Returns structured error format |
// apps/api/src/routes/__tests__/post.routes.test.ts
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from '../../index';
describe('POST /api/v1/posts', () => {
it('returns 401 without auth token', async () => {
const res = await request(app).post('/api/v1/posts').send({});
expect(res.status).toBe(401);
});
it('returns 400 for invalid body', async () => {
const res = await request(app)
.post('/api/v1/posts')
.set('Authorization', `Bearer ${validToken}`)
.send({ caption: '' }); // Missing required fields
expect(res.status).toBe(400);
expect(res.body.error.code).toBe('VALIDATION_ERROR');
});
});
Validators (Schema Correctness)
| Test Category | What to Verify |
|---|---|
| Valid input | Schema parses successfully |
| Missing required fields | Schema rejects with appropriate error |
| Invalid types | Schema rejects wrong data types |
| Edge cases | Min/max length, empty arrays, special characters |
// packages/shared/src/validators/__tests__/post.validators.test.ts
import { describe, it, expect } from 'vitest';
import { createPostSchema } from '../post.validators';
describe('createPostSchema', () => {
it('accepts valid input', () => {
const result = createPostSchema.safeParse({
caption: 'Hello world',
platforms: ['FACEBOOK'],
});
expect(result.success).toBe(true);
});
it('rejects empty caption', () => {
const result = createPostSchema.safeParse({
caption: '',
platforms: ['FACEBOOK'],
});
expect(result.success).toBe(false);
});
it('rejects empty platforms array', () => {
const result = createPostSchema.safeParse({
caption: 'Hello',
platforms: [],
});
expect(result.success).toBe(false);
});
});
React Components
| Test Category | What to Verify |
|---|---|
| Rendering | Component renders without crashing |
| User interactions | Clicks, form inputs trigger correct behavior |
| Loading states | Skeleton/spinner shown during loading |
| Error states | Error message displayed on API failure |
| Conditional rendering | Elements shown/hidden based on props/state |
// apps/web/src/pages/posts/__tests__/PostList.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { PostList } from '../PostList';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
describe('PostList', () => {
it('shows loading skeleton initially', () => {
render(
<QueryClientProvider client={new QueryClient()}>
<PostList />
</QueryClientProvider>
);
expect(screen.getByTestId('skeleton')).toBeInTheDocument();
});
});
Test Priorities
| Priority | What | Why |
|---|---|---|
| High | Service business logic | Core correctness |
| High | Zod validators | Shared validation contract |
| Medium | API route integration tests | Auth + validation + response format |
| Medium | Critical UI flows (auth, composer) | User-facing impact |
| Lower | Utility functions | Usually simple and obvious |
| Lower | Layout components | Minimal logic |
Cross-Reference
- Code Style -- coding conventions
- PR Process -- CI checks require passing tests
- Shared Validators -- schemas to test
- Services -- business logic to test