You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 10:27:03