T

Testing

Type-safe unit and integration testing in NestJS with Jest and proper mocking.

Code

typescript
1// user.service.spec.ts
2import { Test, TestingModule } from "@nestjs/testing";
3import { NotFoundException, ConflictException } from "@nestjs/common";
4import { UserService } from "./user.service";
5import { UserRepository } from "./user.repository";
6import { CreateUserDto } from "./dto/create-user.dto";
7
8// Mock types
9type MockUserRepository = {
10 [K in keyof UserRepository]: jest.Mock;
11};
12
13const createMockRepository = (): MockUserRepository => ({
14 findAll: jest.fn(),
15 findOne: jest.fn(),
16 findByEmail: jest.fn(),
17 create: jest.fn(),
18 update: jest.fn(),
19 delete: jest.fn(),
20});
21
22describe("UserService", () => {
23 let service: UserService;
24 let repository: MockUserRepository;
25
26 const mockUser = {
27 id: 1,
28 name: "Test User",
29 email: "test@example.com",
30 role: "user" as const,
31 createdAt: new Date(),
32 updatedAt: new Date(),
33 };
34
35 beforeEach(async () => {
36 const module: TestingModule = await Test.createTestingModule({
37 providers: [
38 UserService,
39 {
40 provide: UserRepository,
41 useValue: createMockRepository(),
42 },
43 ],
44 }).compile();
45
46 service = module.get<UserService>(UserService);
47 repository = module.get(UserRepository);
48 });
49
50 afterEach(() => {
51 jest.clearAllMocks();
52 });
53
54 describe("findOne", () => {
55 it("should return a user if found", async () => {
56 repository.findOne.mockResolvedValue(mockUser);
57
58 const result = await service.findOne(1);
59
60 expect(result).toEqual(mockUser);
61 expect(repository.findOne).toHaveBeenCalledWith(1);
62 });
63
64 it("should throw NotFoundException if user not found", async () => {
65 repository.findOne.mockResolvedValue(null);
66
67 await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
68 });
69 });
70
71 describe("create", () => {
72 const createDto: CreateUserDto = {
73 name: "New User",
74 email: "new@example.com",
75 password: "password123",
76 };
77
78 it("should create a new user", async () => {
79 repository.findByEmail.mockResolvedValue(null);
80 repository.create.mockResolvedValue({ id: 2, ...createDto, role: "user" });
81
82 const result = await service.create(createDto);
83
84 expect(result).toHaveProperty("id");
85 expect(repository.create).toHaveBeenCalled();
86 });
87
88 it("should throw ConflictException if email exists", async () => {
89 repository.findByEmail.mockResolvedValue(mockUser);
90
91 await expect(service.create(createDto)).rejects.toThrow(ConflictException);
92 });
93 });
94
95 describe("update", () => {
96 it("should update an existing user", async () => {
97 repository.findOne.mockResolvedValue(mockUser);
98 repository.update.mockResolvedValue({ ...mockUser, name: "Updated" });
99
100 const result = await service.update(1, { name: "Updated" });
101
102 expect(result.name).toBe("Updated");
103 });
104 });
105
106 describe("delete", () => {
107 it("should delete an existing user", async () => {
108 repository.findOne.mockResolvedValue(mockUser);
109 repository.delete.mockResolvedValue(undefined);
110
111 await expect(service.delete(1)).resolves.not.toThrow();
112 });
113 });
114});
115
116// user.controller.spec.ts
117import { Test, TestingModule } from "@nestjs/testing";
118import { UserController } from "./user.controller";
119import { UserService } from "./user.service";
120
121describe("UserController", () => {
122 let controller: UserController;
123 let service: jest.Mocked<UserService>;
124
125 const mockUserService = {
126 findAll: jest.fn(),
127 findOne: jest.fn(),
128 create: jest.fn(),
129 update: jest.fn(),
130 delete: jest.fn(),
131 };
132
133 beforeEach(async () => {
134 const module: TestingModule = await Test.createTestingModule({
135 controllers: [UserController],
136 providers: [
137 {
138 provide: UserService,
139 useValue: mockUserService,
140 },
141 ],
142 }).compile();
143
144 controller = module.get<UserController>(UserController);
145 service = module.get(UserService);
146 });
147
148 describe("findAll", () => {
149 it("should return array of users", async () => {
150 const mockUsers = [{ id: 1, name: "User 1" }];
151 service.findAll.mockResolvedValue({ data: mockUsers, total: 1 });
152
153 const result = await controller.findAll({ page: 1, limit: 10 });
154
155 expect(result.data).toEqual(mockUsers);
156 });
157 });
158});
159
160// app.e2e-spec.ts - Integration test
161import { Test, TestingModule } from "@nestjs/testing";
162import { INestApplication, ValidationPipe } from "@nestjs/common";
163import * as request from "supertest";
164import { AppModule } from "../src/app.module";
165
166describe("UserController (e2e)", () => {
167 let app: INestApplication;
168
169 beforeAll(async () => {
170 const moduleFixture: TestingModule = await Test.createTestingModule({
171 imports: [AppModule],
172 }).compile();
173
174 app = moduleFixture.createNestApplication();
175 app.useGlobalPipes(new ValidationPipe());
176 await app.init();
177 });
178
179 afterAll(async () => {
180 await app.close();
181 });
182
183 describe("/users (POST)", () => {
184 it("should create a user", () => {
185 return request(app.getHttpServer())
186 .post("/users")
187 .send({
188 name: "Test User",
189 email: "test@example.com",
190 password: "password123",
191 })
192 .expect(201)
193 .expect((res) => {
194 expect(res.body).toHaveProperty("id");
195 expect(res.body.email).toBe("test@example.com");
196 });
197 });
198
199 it("should return 400 for invalid data", () => {
200 return request(app.getHttpServer())
201 .post("/users")
202 .send({ name: "" })
203 .expect(400);
204 });
205 });
206
207 describe("/users (GET)", () => {
208 it("should return users list", () => {
209 return request(app.getHttpServer())
210 .get("/users")
211 .expect(200)
212 .expect((res) => {
213 expect(Array.isArray(res.body.data)).toBe(true);
214 });
215 });
216 });
217});

Run this example locally

$ npm install --save-dev @nestjs/testing jest @types/jest supertest