Unit Testing
PrimeCal's unit tests use Jest with ts-jest and @nestjs/testing. Each unit test isolates a single class by mocking all its dependencies — no real database connections, no real HTTP requests.
Running Unit Tests
# Backend
cd backend-nestjs
npm run test # all *.spec.ts under src/, no coverage
npm run test:unit # with coverage report in coverage/
npm run test:unit:strict # with 80% threshold enforcement
npm run test:watch # watch mode
# Frontend
cd frontend
npm run test:unit # Jest, ignores *.integration.test.* files
npm run test:watch
Backend Unit Test Anatomy
Unit tests live alongside their source files in src/. The naming convention is <source-file>.spec.ts.
Minimal Service Test
// src/tasks/tasks.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NotFoundException } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { Task } from '../entities/task.entity';
describe('TasksService', () => {
let service: TasksService;
const mockTaskRepo = {
findOne: jest.fn(),
find: jest.fn(),
save: jest.fn(),
create: jest.fn(),
softDelete: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TasksService,
{ provide: getRepositoryToken(Task), useValue: mockTaskRepo },
],
}).compile();
service = module.get<TasksService>(TasksService);
});
afterEach(() => jest.clearAllMocks());
describe('findOne', () => {
it('returns task when found', async () => {
const mockTask = { id: 1, title: 'Write tests', userId: 42 };
mockTaskRepo.findOne.mockResolvedValue(mockTask);
const result = await service.findOne(1, 42);
expect(result).toEqual(mockTask);
expect(mockTaskRepo.findOne).toHaveBeenCalledWith({
where: { id: 1, userId: 42 },
});
});
it('throws NotFoundException when task does not exist', async () => {
mockTaskRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(999, 42)).rejects.toThrow(NotFoundException);
});
});
});
Testing Guards
Guards use a mock ExecutionContext:
// src/common/guards/active-organisation.guard.spec.ts
import { ActiveOrganisationGuard } from './active-organisation.guard';
import { BadRequestException } from '@nestjs/common';
import type { ExecutionContext } from '@nestjs/core/interfaces';
describe('ActiveOrganisationGuard', () => {
const guard = new ActiveOrganisationGuard();
function makeContext(activeOrganisationId: unknown): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => ({ user: { activeOrganisationId } }),
}),
} as unknown as ExecutionContext;
}
it('passes when activeOrganisationId is a positive integer', () => {
expect(guard.canActivate(makeContext(5))).toBe(true);
});
it('throws BadRequestException when activeOrganisationId is missing', () => {
expect(() => guard.canActivate(makeContext(undefined))).toThrow(BadRequestException);
});
it('throws BadRequestException when activeOrganisationId is zero', () => {
expect(() => guard.canActivate(makeContext(0))).toThrow(BadRequestException);
});
});
Testing DTO Validation
Use class-validator's validate() function to test DTO decorator behavior directly:
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { CreateOrganisationDto } from '../dto/organisation.dto';
describe('CreateOrganisationDto', () => {
it('accepts valid input', async () => {
const dto = plainToInstance(CreateOrganisationDto, { name: 'My Org' });
const errors = await validate(dto);
expect(errors).toHaveLength(0);
});
it('rejects empty name', async () => {
const dto = plainToInstance(CreateOrganisationDto, { name: '' });
const errors = await validate(dto);
expect(errors.some(e => e.property === 'name')).toBe(true);
});
});
Testing Controllers
Controllers are tested by overriding guards and mocking the service:
describe('TasksController', () => {
let controller: TasksController;
const mockService = {
findAll: jest.fn().mockResolvedValue([]),
create: jest.fn(),
};
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [TasksController],
providers: [{ provide: TasksService, useValue: mockService }],
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get(TasksController);
});
});
Frontend Unit Tests
Frontend unit tests use Jest with @testing-library/react and jest-environment-jsdom.
// src/hooks/useCalendarSettings.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCalendarSettings } from './useCalendarSettings';
describe('useCalendarSettings', () => {
it('returns default 12h time format', () => {
const { result } = renderHook(() => useCalendarSettings());
expect(result.current.timeFormat).toBe('12h');
});
});
What to Test in Unit Tests
| Class type | Key things to test |
|---|---|
| Services | Success path, NotFoundException, ForbiddenException, duplicate detection |
| Guards | canActivate returns true with valid context; throws with invalid context |
| DTOs | Valid input passes; missing required fields fail; type constraints enforced |
| Controllers | Correct service method is called; response shape matches; guard bypass |
| Executors | Action executor outputs correct entity mutations |
Mocking Best Practices
- Use
jest.fn()for all repository methods and collaborating service methods - Always call
jest.clearAllMocks()inafterEachto prevent test pollution - Use
mockResolvedValue(for async) ormockReturnValue(for sync) — notmockImplementationunless the mock logic is non-trivial - Do not mock the class under test — only its dependencies
SQL Injection Tests
PrimeCal maintains dedicated SQL injection test files for services that accept user search input:
src/tasks/tasks.service.sql-injection.spec.tssrc/users/users.service.sql-injection.spec.ts
If you add a new service with a search or free-text filter, add a corresponding SQL injection spec file.