Testing (TypeScript)
APSO-generated NestJS projects include Jest for testing. This guide covers unit, integration, and E2E testing patterns.
Test Structure
my-api/
├── src/
│ └── modules/
│ └── projects/
│ ├── projects.service.spec.ts # Unit tests
│ └── projects.controller.spec.ts # Unit tests
└── test/
├── app.e2e-spec.ts # E2E tests
└── jest-e2e.json # E2E configRunning Tests
# Run unit tests
npm test
# Run with coverage
npm run test:cov
# Run E2E tests
npm run test:e2e
# Watch mode
npm run test:watchUnit Testing Services
Test services in isolation with mocked dependencies:
// src/modules/projects/projects.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ProjectsService } from './projects.service';
import { Project } from '../../entities/project.entity';
describe('ProjectsService', () => {
let service: ProjectsService;
let mockRepository: jest.Mocked<Repository<Project>>;
const mockProject = {
id: 'uuid-1',
name: 'Test Project',
organizationId: 'org-1',
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
ProjectsService,
{
provide: getRepositoryToken(Project),
useValue: mockRepository,
},
],
}).compile();
service = module.get<ProjectsService>(ProjectsService);
});
describe('findAll', () => {
it('should return projects for organization', async () => {
mockRepository.find.mockResolvedValue([mockProject]);
const result = await service.findAll(
{ limit: 10, offset: 0 },
{ organizationId: 'org-1' } as any,
);
expect(result).toEqual([mockProject]);
expect(mockRepository.find).toHaveBeenCalledWith({
where: { organizationId: 'org-1' },
take: 10,
skip: 0,
});
});
});
describe('create', () => {
it('should create a project', async () => {
const createDto = { name: 'New Project' };
mockRepository.create.mockReturnValue(mockProject as any);
mockRepository.save.mockResolvedValue(mockProject);
const result = await service.create(
createDto,
{ organizationId: 'org-1' } as any,
);
expect(result).toEqual(mockProject);
expect(mockRepository.create).toHaveBeenCalledWith({
...createDto,
organizationId: 'org-1',
});
});
});
describe('findOne', () => {
it('should return a project', async () => {
mockRepository.findOne.mockResolvedValue(mockProject);
const result = await service.findOne('uuid-1', { organizationId: 'org-1' } as any);
expect(result).toEqual(mockProject);
});
it('should throw if not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(
service.findOne('invalid', { organizationId: 'org-1' } as any),
).rejects.toThrow('Project not found');
});
});
});Unit Testing Controllers
Test controllers with mocked services:
// src/modules/projects/projects.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ProjectsController } from './projects.controller';
import { ProjectsService } from './projects.service';
describe('ProjectsController', () => {
let controller: ProjectsController;
let mockService: jest.Mocked<ProjectsService>;
const mockUser = {
id: 'user-1',
organizationId: 'org-1',
};
beforeEach(async () => {
mockService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
controllers: [ProjectsController],
providers: [
{
provide: ProjectsService,
useValue: mockService,
},
],
}).compile();
controller = module.get<ProjectsController>(ProjectsController);
});
describe('findAll', () => {
it('should return paginated projects', async () => {
const mockResult = {
data: [{ id: '1', name: 'Test' }],
total: 1,
};
mockService.findAll.mockResolvedValue(mockResult);
const result = await controller.findAll(
{ limit: 10, offset: 0 },
mockUser as any,
);
expect(result).toEqual(mockResult);
expect(mockService.findAll).toHaveBeenCalledWith(
{ limit: 10, offset: 0 },
mockUser,
);
});
});
describe('create', () => {
it('should create and return a project', async () => {
const dto = { name: 'New Project' };
const mockProject = { id: '1', ...dto };
mockService.create.mockResolvedValue(mockProject);
const result = await controller.create(dto, mockUser as any);
expect(result).toEqual(mockProject);
});
});
});E2E Testing
Test the full API with HTTP requests:
// test/projects.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Projects (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.setGlobalPrefix('api/v1');
await app.init();
// Get auth token
const loginRes = await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({ email: 'test@example.com', password: 'password' });
authToken = loginRes.body.accessToken;
});
afterAll(async () => {
await app.close();
});
describe('GET /api/v1/projects', () => {
it('should require authentication', () => {
return request(app.getHttpServer())
.get('/api/v1/projects')
.expect(401);
});
it('should return projects', () => {
return request(app.getHttpServer())
.get('/api/v1/projects')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('data');
expect(Array.isArray(res.body.data)).toBe(true);
});
});
});
describe('POST /api/v1/projects', () => {
it('should create a project', () => {
return request(app.getHttpServer())
.post('/api/v1/projects')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'E2E Test Project' })
.expect(201)
.expect((res) => {
expect(res.body.name).toBe('E2E Test Project');
expect(res.body.id).toBeDefined();
});
});
it('should validate input', () => {
return request(app.getHttpServer())
.post('/api/v1/projects')
.set('Authorization', `Bearer ${authToken}`)
.send({}) // Missing required name
.expect(400);
});
});
});Test Database
Use a separate test database:
// test/setup.ts
import { TypeOrmModule } from '@nestjs/typeorm';
export const TestDatabaseModule = TypeOrmModule.forRoot({
type: 'postgres',
url: process.env.TEST_DATABASE_URL,
autoLoadEntities: true,
synchronize: true, // OK for test database
});Mocking Patterns
Mock Repository
const mockRepository = {
find: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockResolvedValue(null),
create: jest.fn().mockImplementation((dto) => dto),
save: jest.fn().mockImplementation((entity) => ({ id: 'uuid', ...entity })),
delete: jest.fn().mockResolvedValue({ affected: 1 }),
};Mock External Services
const mockEmailService = {
send: jest.fn().mockResolvedValue(true),
};
const module = await Test.createTestingModule({
providers: [
MyService,
{ provide: EmailService, useValue: mockEmailService },
],
}).compile();Code Coverage
Generate coverage reports:
npm run test:covConfigure in jest.config.js:
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/main.ts',
],
};Related
Last updated on