How to write, run, and maintain tests for the RetroLoop project.
# Backend
yarn workspace back test # Run unit tests
yarn workspace back test:watch # Watch mode
yarn workspace back test --coverage # With coverage report
yarn workspace back test:e2e # E2E tests (requires PostgreSQL)
# Frontend
yarn workspace front test # Run unit tests
yarn workspace front test:watch # Watch mode
yarn workspace front test:coverage # With coverage report| Backend | Frontend | |
|---|---|---|
| Framework | Jest 30 | Jest 30 |
| Environment | node |
jsdom |
| Config | apps/back/package.json (inline jest key) |
apps/front/jest.config.ts |
| Setup file | apps/back/test/setup-e2e.ts (E2E only) |
apps/front/jest.setup.ts |
| Assertion lib | Jest built-in | Jest + @testing-library/jest-dom |
| Rendering | — | @testing-library/react |
| User events | — | @testing-library/user-event |
| HTTP testing | supertest (E2E) |
— |
Tests live alongside source code in __tests__/ directories:
apps/back/src/modules/voting/
├── voting.service.ts
└── __tests__/
└── voting.service.spec.ts # ← unit test
apps/front/components/ui/
├── button.tsx
└── __tests__/
└── button.test.tsx # ← component test
apps/back/test/
└── health.e2e-spec.ts # ← E2E test
| Pattern | Use |
|---|---|
*.spec.ts |
Backend unit / integration tests |
*.test.tsx |
Frontend component tests |
*.e2e-spec.ts |
Backend end-to-end tests |
Backend tests use the NestJS Test.createTestingModule() to wire up the class under test with mocked dependencies.
import { Test, TestingModule } from '@nestjs/testing';
import { WsException } from '@nestjs/websockets';
import { CardHandler } from '../card.handler';
import { CardsService } from '../../../modules/cards/cards.service';
import { WsAuthorizationService } from '../../ws-authorization.service';
describe('CardHandler', () => {
let handler: CardHandler;
let cardsService: jest.Mocked<CardsService>;
// Mock the Socket.IO server
const mockEmit = jest.fn();
const mockServer = {
to: jest.fn().mockReturnValue({ emit: mockEmit }),
} as any;
beforeEach(async () => {
mockEmit.mockClear();
mockServer.to.mockClear();
mockServer.to.mockReturnValue({ emit: mockEmit });
const module: TestingModule = await Test.createTestingModule({
providers: [
CardHandler,
{
provide: CardsService,
useValue: {
createWithLimitCheck: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
{
provide: WsAuthorizationService,
useValue: {
verifyRetroAccess: jest.fn().mockResolvedValue(undefined),
},
},
],
}).compile();
handler = module.get<CardHandler>(CardHandler);
cardsService = module.get(CardsService);
});
describe('handleCreateCard', () => {
it('should emit card:created to retro room', async () => {
cardsService.createWithLimitCheck.mockResolvedValue(mockCard);
await handler.handleCreateCard('user-1', createData, mockServer);
expect(mockServer.to).toHaveBeenCalledWith('retro-1');
expect(mockEmit).toHaveBeenCalledWith('card:created', mockCard);
});
it('should throw WsException on validation failure', async () => {
const longContent = { ...createData, content: 'a'.repeat(5001) };
await expect(
handler.handleCreateCard('user-1', longContent, mockServer),
).rejects.toThrow(WsException);
});
});
});- Mock all dependencies — provide every injected service via
useValuewithjest.fn()methods. - Use
jest.Mocked<T>— gives you type-safe access to.mockResolvedValue(),.mockRejectedValue(), etc. - Clear mocks in
beforeEach— prevents state leaking between tests. - Test both success and error paths — verify the handler wraps service errors in
WsException. - Mock the Socket.IO server — use
{ to: jest.fn().mockReturnValue({ emit: mockEmit }) }to verify room broadcasts.
When a module uses a third-party library (e.g., bcrypt), mock it before importing the module under test:
const mockBcryptCompare = jest.fn();
const mockBcryptHash = jest.fn();
jest.mock('bcrypt', () => ({
compare: (...args: unknown[]) => mockBcryptCompare(...args),
hash: (...args: unknown[]) => mockBcryptHash(...args),
}));E2E tests use the full NestJS application with supertest. The setup file (apps/back/test/setup-e2e.ts) configures environment variables:
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL =
process.env.DATABASE_URL || 'postgresql://test:test@localhost:5432/retro_test';
process.env.JWT_SECRET =
process.env.JWT_SECRET || 'test-jwt-secret-key-must-be-at-least-32-characters-long';Example E2E test:
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('HealthController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(DATABASE_CONNECTION)
.useValue(mockDb)
.compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterEach(async () => {
if (app) await app.close();
});
it('/health (GET)', () => {
return request(app.getHttpServer()).get('/health').expect(200);
});
});Frontend tests use @testing-library/react and @testing-library/user-event:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../button';
describe('Button', () => {
it('renders with children text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick handler when clicked', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick} disabled>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
});- Query by role/label — prefer
screen.getByRole(),screen.getByText()overgetByTestId(). Usedata-testidas a last resort. - Use
userEvent.setup()— simulates real browser interactions more accurately thanfireEvent. - Use
waitFor()for async updates — when testing context providers, async state changes, or API calls. - Mock API client — mock the
apiClientmodule when testing components that make API calls:
jest.mock('../../lib/api-client', () => ({
apiClient: {
getProfile: jest.fn(),
login: jest.fn(),
logout: jest.fn(),
},
}));Components using framer-motion should mock it to avoid animation issues:
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}));The frontend setup file provides:
@testing-library/jest-dom— adds custom matchers like.toBeInTheDocument(),.toHaveAttribute().- Next.js navigation mock — stubs
useRouter,usePathname,useSearchParams. window.matchMediamock — prevents errors from responsive components.- Console suppression —
console.errorandconsole.warnare silenced by default. If you need to assert on console output, restore it in your test.
Coverage thresholds are enforced by Jest. The build fails if coverage drops below these minimums:
| Branches | Functions | Lines | Statements | |
|---|---|---|---|---|
| Backend | 30% | 30% | 30% | 30% |
| Frontend | 10% | 10% | 10% | 10% |
Collects from all **/*.(t|j)s files under apps/back/src/. Output goes to apps/back/coverage/.
Collects from these directories:
app/**components/**contexts/**hooks/**lib/**
Excludes: .d.ts files, node_modules/, .next/, coverage/.
# Generate and open HTML coverage report (backend)
yarn workspace back test --coverage
open apps/back/coverage/lcov-report/index.html
# Frontend
yarn workspace front test:coverage
open apps/front/coverage/lcov-report/index.htmlTests run automatically on every push and pull request via GitHub Actions (.github/workflows/ci.yml). The pipeline has four test-related jobs:
Runs linting and type checking (builds both apps).
- Starts a PostgreSQL 16 service container.
- Runs
yarn workspace back test --coverage. - Uploads coverage to Codecov with the
backendflag.
- Runs
yarn workspace front test --coverage. - Uploads coverage to Codecov with the
frontendflag.
- Starts a PostgreSQL 16 service container.
- Sets
DATABASE_URLandJWT_SECRETfrom CI secrets. - Runs
yarn workspace back test:e2e.
All test jobs must pass before a PR can be merged.
- Unit tests for service/handler logic.
- Component tests for new UI components.
- Test both success and error paths.
- Write a test that reproduces the bug first, then fix the code. This prevents regressions.
- Mock the
WsAuthorizationServiceand domain services. - Verify the correct event is emitted to the correct room.
- Test authorization rejection paths.
- Test that the guard returns
true/falsefor valid / invalid inputs. - Test edge cases (missing tokens, suspended users, expired JWTs).
- Auto-generated boilerplate (module definitions, barrel exports).
- Pure type definitions.
- Third-party library internals.
yarn workspace back test -- --testPathPattern="card.handler"
yarn workspace front test -- --testPathPattern="button"yarn workspace back test -- -t "should emit card:created"yarn workspace back test:debug -- --testPathPattern="card.handler"Then attach VS Code or Chrome DevTools to the Node inspector on port 9229.
yarn workspace back test:watch
yarn workspace front test:watchPress p to filter by filename pattern, t to filter by test name.
| Problem | Solution |
|---|---|
Cannot find module '@retro/shared-types' |
Run yarn workspace @retro/shared-types build-lib first |
| E2E tests fail with connection errors | Ensure PostgreSQL is running (docker-compose up -d) |
Frontend test fails with act() warnings |
Wrap async state updates in waitFor() |
| Mock not resetting between tests | Add jest.clearAllMocks() or mockFn.mockClear() in beforeEach |
framer-motion errors in tests |
Mock it (see Mocking framer-motion) |
useRouter not found |
Already handled by jest.setup.ts; if you need custom values, mock next/navigation in your test |