如何使用Jest为Express项目的CreateUserController编写正确单元测试
Express Controller 单元测试解决方案
你的现有代码完全支持单元测试,现有测试代码的核心问题有3个:
- 未正确实例化Controller,且调用
handle方法时没有传入符合要求的Request、Response参数 - 没有隔离外部依赖:Controller内部直接实例化
CreateUserService,测试时没有对Service做mock,导致测试逻辑渗透到Service甚至数据库层 - 调用方法的参数格式不符合预期:直接把用户数据当第一个参数传给
handle,而不是放在Request的body属性里
最小改动测试方案(无需修改原有业务代码)
我们利用Jest的模块Mock能力,直接模拟CreateUserService、Express的请求响应对象即可完成Controller的单元测试,不需要触碰数据库、bcrypt等外部依赖。
正确的测试代码
// CreateUserController.unit.test.ts import { CreateUserController } from "./CreateUserController"; // 导入Service用来做Mock import { CreateUserService } from "../services/CreateUserService"; import type { Request, Response } from "express"; // 全局Mock整个CreateUserService模块 jest.mock("../services/CreateUserService"); // 类型标注,方便TS识别Mock方法 const mockCreateUserService = CreateUserService as jest.MockedClass<typeof CreateUserService>; describe('CreateUserController 单元测试', () => { let createUserController: CreateUserController; // 模拟Response对象,send方法用jest.fn()方便断言调用情况 let mockResponse: Partial<Response>; // 每个用例执行前重置mock,避免用例之间互相影响 beforeEach(() => { createUserController = new CreateUserController(); mockResponse = { send: jest.fn() }; // 重置Service的mock实现 mockCreateUserService.mockClear(); }); it('参数正确时应返回用户数据', async () => { // 构造测试数据 const mockUser = { id: 'test-uuid', name: 'Fulano', email: 'email@d.com', admin: true, created_at: new Date(), updated_at: new Date() }; // 模拟Service的execute方法返回测试数据 mockCreateUserService.prototype.execute.mockResolvedValue(mockUser); // 构造Request对象,body放用户参数 const mockRequest = { body: { name: 'Fulano', email: 'email@d.com', password: '123456', admin: true } } as Request; // 调用Controller的handle方法 await createUserController.handle(mockRequest, mockResponse as Response); // 断言:Service的execute被正确调用,参数和请求body一致 expect(mockCreateUserService.prototype.execute).toHaveBeenCalledWith({ name: 'Fulano', email: 'email@d.com', password: '123456', admin: true }); // 断言:response.send被调用,返回了正确的用户数据 expect(mockResponse.send).toHaveBeenCalledWith(mockUser); }); it('Service抛出错误时应返回错误信息', async () => { const mockError = new Error('测试错误'); // 模拟Service的execute方法抛出错误 mockCreateUserService.prototype.execute.mockRejectedValue(mockError); const mockRequest = { body: { name: 'Fulano', email: 'wrong-email', password: '123456' } } as Request; await createUserController.handle(mockRequest, mockResponse as Response); // 断言:response.send返回了错误对象 expect(mockResponse.send).toHaveBeenCalledWith({ error: mockError }); }); });
可选优化:依赖注入改造(可测性更强)
如果后续要扩展功能,建议对现有代码做简单的依赖注入改造,不需要依赖jest的模块mock也能轻松测试:
修改CreateUserController.ts,把Service作为参数注入构造函数:
// 修改后的CreateUserController.ts import { Request, Response } from "express"; import { CreateUserService } from "../services/CreateUserService"; class CreateUserController { // 注入Service实例,测试时可以直接传入mock的Service constructor(private createUserService: CreateUserService = new CreateUserService()) {} async handle(request: Request, response: Response) { try { const { name, email, admin, password } = request.body; const user = await this.createUserService.execute({ name, email, admin, password }); return response.send(user); } catch (error) { return response.send({ error }); } } } export { CreateUserController };
改造后测试时不需要mock整个模块,直接传入mock的Service实例即可,测试逻辑会更清晰。
内容的提问来源于stack exchange,提问作者João Casarin
相关产品推荐
相关产品推荐

