Testing¶
1. Overview¶
Tests use Jest 30 + ts-jest in ESM mode. Unit tests mock PrismaService and never touch a real database. E2E tests spin up the full NestJS application against a real (test) database and use Supertest for HTTP assertions.
Every test file must have /* eslint-disable */ at the very top — jest mocks are loosely typed and trigger @typescript-eslint/no-unsafe-* rules.
2. Service Specs¶
Service tests create a NestJS testing module with a manually crafted PrismaService mock. Only stub the Prisma delegates and methods your service actually calls.
/* eslint-disable */
const prisma = {
student: {
create: jest.fn().mockResolvedValue(mockStudent),
findMany: jest.fn(), findFirst: jest.fn(),
updateMany: jest.fn(), deleteMany: jest.fn(),
count: jest.fn().mockResolvedValue(0),
},
academicYear: { findFirst: jest.fn().mockResolvedValue({ id: 'year-1', status: 'ACTIVE' }) },
$transaction: jest.fn((fn: any) => fn(prisma)),
};
const module = await Test.createTestingModule({
providers: [
StudentsService,
{ provide: PrismaService, useValue: prisma },
{ provide: CustomFieldsService, useValue: createMockCustomFieldsService() },
],
}).compile();
Key points:
$transactionmock passes the sameprismaobject as the transaction client, so transactional calls resolve correctly.- Reset mocks in
beforeEachwithjest.clearAllMocks()to prevent state leaking between tests. - Only stub the Prisma delegates the service under test actually uses.
Canonical example: src/students/students.service.spec.ts
3. Controller Specs¶
Controller tests bypass the full guard stack using the overrideAuthGuards helper from src/common/testing/. This replaces JwtAuthGuard, ScopeGuard, ActionGuard, FieldWriteGuard, and FieldFilterInterceptor with no-op stubs so HTTP routing can be tested in isolation.
/* eslint-disable */
import { overrideAuthGuards } from '../common/testing';
const module = await overrideAuthGuards(
Test.createTestingModule({ controllers: [StudentsController], providers: [...] })
).compile();
For setup wizard controller specs, also assert the @HttpCode(200) decorator via reflection:
const httpCode = Reflect.getMetadata('__httpCode__', SetupController.prototype.submitStep);
expect(httpCode).toBe(200);
Canonical example: src/setup/setup.controller.spec.ts
4. Test Helpers¶
All helpers live in src/common/testing/.
| Helper | Use |
|---|---|
createMockCustomFieldsService() |
Mock CustomFieldsService for domain service tests |
overrideAuthGuards(builder) |
Override all auth guards + FieldFilterInterceptor for controller tests |
createMockAuthRequest(overrides?) |
Mock AuthenticatedRequest for controller tests |
createMockContext(user?, body?, permissions?) |
Mock ExecutionContext for guard unit tests |
5. Test Case Categories¶
Standard service spec categories for domain CRUD modules (see chapter 05 for the underlying service patterns):
- create — happy path + validation (grade lookup, duplicate detection, auto-generated codes)
- findAll — paginated response, tenant isolation
- findOne — found + not found
- update — partial update, not found
- remove — success + not found
- toScopedResponse — scope grouping, field mapping
- Entity-specific — import pipeline, lifecycle hooks
What to test per layer:
| Layer | What to test | What to mock |
|---|---|---|
| Service | Business logic, error cases, data transformations | PrismaService, other services |
| Guard | Allow/deny decisions | Reflector, PermissionsService |
| Interceptor | Response transformation | PermissionsService, Reflector |
| Controller | Delegation + decorator metadata | Service, guards (via overrideAuthGuards) |
6. Setup Wizard Testing¶
See chapter 08 for the wizard architecture. The test strategy has four layers:
DTO specs¶
Use plainToInstance() + validate() from class-validator directly — no NestJS DI needed. Define a toDto() helper that merges overrides onto a validData constant:
function toDto(overrides: Record<string, unknown> = {}) {
return plainToInstance(SchoolStepDataDto, { ...validData, ...overrides });
}
it('should fail country with invalid codes', async () => {
for (const code of ['XX', 'usa', '']) {
const errors = await validate(toDto({ country: code }));
expect(errors.some((e) => e.property === 'country')).toBe(true);
}
});
Canonical: src/setup/dto/steps/school-step.dto.spec.ts
Handler specs¶
Handlers are plain functions — test them without the NestJS DI container. Mock PrismaService as a plain object. Pass the same mock object as the transaction client:
Test all three methods (load, save, isComplete) plus error paths (missing academic year, duplicate names, ordinal gaps).
Canonical: src/setup/step-handlers/departments.handler.spec.ts
Service spec¶
Full state machine coverage (~1100 lines). Uses Test.createTestingModule with a mocked PrismaService. Covers: state transitions, mismatch errors, data loading per step, forward/backward/same-step navigation, completion gating.
Canonical: src/setup/setup.service.spec.ts
Controller spec¶
Thin delegation tests verifying the controller passes tenantId to the service. Also asserts @HttpCode(200) metadata on the POST endpoint via Reflect.getMetadata().
Canonical: src/setup/setup.controller.spec.ts
7. E2E Tests¶
E2E tests live in the test/ folder and spin up the full application against a real database:
// test/teachers.e2e-spec.ts
describe('Teachers (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => { await app.close(); });
it('/teachers (GET) should require authentication', () => {
return request(app.getHttpServer()).get('/api/v1/teachers').expect(401);
});
});
Run with npm run test:e2e. E2E tests require a live database — use the Docker stack (npm run docker:up) or a dedicated test DB.
8. Running Tests¶
npm test # All unit tests
npm run test:watch # Re-run on file changes
npm test -- --testPathPatterns=students # Single module
npm run test:cov # Coverage report
npm run test:e2e # E2E (requires DB)
9. Rules¶
/* eslint-disable */at the top of every test file — mocks triggerno-unsafe-*rules.- Never hit a real database in unit tests — mock
PrismaServicewithuseValue. - Mock
$transactionasjest.fn((fn) => fn(prisma))so transactional code resolves correctly with the same mock client. - Use
AppException(not NestJS built-ins) in production code — test that the correctErrorCodeis thrown, not the HTTP exception class. - Canonical unit test example:
src/students/students.service.spec.ts - Canonical setup test examples:
src/setup/setup.service.spec.ts,src/setup/step-handlers/departments.handler.spec.ts