Skip to content

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:

  • $transaction mock passes the same prisma object as the transaction client, so transactional calls resolve correctly.
  • Reset mocks in beforeEach with jest.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):

  1. create — happy path + validation (grade lookup, duplicate detection, auto-generated codes)
  2. findAll — paginated response, tenant isolation
  3. findOne — found + not found
  4. update — partial update, not found
  5. remove — success + not found
  6. toScopedResponse — scope grouping, field mapping
  7. 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:

const prisma = {
  department: { findMany: jest.fn() },
  $transaction: jest.fn((fn) => fn(prisma)),
};

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 trigger no-unsafe-* rules.
  • Never hit a real database in unit tests — mock PrismaService with useValue.
  • Mock $transaction as jest.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 correct ErrorCode is 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