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

如何用Jest为含Joi校验的ExpressJS端点sendVoiceCall编写单元测试

Express sendVoiceCall 端点单元测试方案

先明确你的端点函数核心逻辑参考(方便对应测试):

const Joi = require('joi');
const userService = require('../services/userService'); // 假设查询用户的服务
const voiceService = require('../services/voiceService'); // 假设语音呼叫服务

exports.sendVoiceCall = async (req, res, next) => {
  // Joi 参数校验
  const schema = Joi.object({
    phoneNo: Joi.string().pattern(/^\+?[1-9]\d{1,14}$/).required()
  });

  const { error } = schema.validate(req.body);
  if (error) {
    return res.status(400).json({ message: error.details[0].message });
  }

  try {
    const user = await userService.getUserByPhone(req.body.phoneNo);
    if (!user) {
      return res.status(404).json({ message: '用户不存在' });
    }

    await voiceService.sendOTP(user.phoneNo);
    return res.status(200).json({ message: '语音呼叫已发送' });
  } catch (err) {
    next(err);
  }
};

错误原因分析

  1. res.status未被调用:模拟res对象时未处理链式调用(比如res.status()没返回res本身),或函数执行到异常分支但未正确捕获。
  2. Joi mock报错:直接调用Joi.object().mockReturnValue无效,因为Joi.object()是实际函数调用,需通过jest.mock整体mock Joi模块后再控制返回值。

正确测试代码实现

1. 基础准备

在测试文件顶部mock依赖:

const Joi = require('joi');
const { sendVoiceCall } = require('../controllers/voiceController'); // 你的控制器路径
const userService = require('../services/userService');
const voiceService = require('../services/voiceService');

// Mock 整个Joi模块
jest.mock('joi');
// Mock 用户服务和语音服务
jest.mock('../services/userService');
jest.mock('../services/voiceService');

// 模拟Express的req、res、next对象
const mockReq = () => ({ body: {} });
const mockRes = () => {
  const res = {};
  res.status = jest.fn().mockReturnThis(); // 关键:链式调用需返回res本身
  res.json = jest.fn().mockReturnThis();
  return res;
};
const mockNext = jest.fn();

2. 测试用例编写

测试1:正常流程(参数合法+用户存在)

test('正常发送语音呼叫', async () => {
  const req = mockReq();
  req.body.phoneNo = '+1234567890';
  const res = mockRes();

  // Mock Joi校验通过
  Joi.object.mockReturnValue({
    validate: jest.fn().mockReturnValue({ error: null })
  });

  // Mock 用户存在
  userService.getUserByPhone.mockResolvedValue({ id: 1, phoneNo: '+1234567890' });

  // Mock 语音呼叫发送成功
  voiceService.sendOTP.mockResolvedValue();

  await sendVoiceCall(req, res, mockNext);

  // 断言验证
  expect(Joi.object).toHaveBeenCalled();
  expect(userService.getUserByPhone).toHaveBeenCalledWith('+1234567890');
  expect(voiceService.sendOTP).toHaveBeenCalled();
  expect(res.status).toHaveBeenCalledWith(200);
  expect(res.json).toHaveBeenCalledWith({ message: '语音呼叫已发送' });
  expect(mockNext).not.toHaveBeenCalled();
});

测试2:用户不存在(参数合法但无此用户)

test('用户不存在返回404', async () => {
  const req = mockReq();
  req.body.phoneNo = '+1234567890';
  const res = mockRes();

  // Mock Joi校验通过
  Joi.object.mockReturnValue({
    validate: jest.fn().mockReturnValue({ error: null })
  });

  // Mock 用户不存在
  userService.getUserByPhone.mockResolvedValue(null);

  await sendVoiceCall(req, res, mockNext);

  expect(userService.getUserByPhone).toHaveBeenCalledWith('+1234567890');
  expect(res.status).toHaveBeenCalledWith(404);
  expect(res.json).toHaveBeenCalledWith({ message: '用户不存在' });
});

测试3:参数校验失败(phoneNo格式错误)

test('参数格式错误返回400', async () => {
  const req = mockReq();
  req.body.phoneNo = 'invalid-phone'; // 非法格式
  const res = mockRes();

  // Mock Joi校验失败,返回错误信息
  const mockValidationError = {
    details: [{ message: '"phoneNo" must be a valid phone number' }]
  };
  Joi.object.mockReturnValue({
    validate: jest.fn().mockReturnValue({ error: mockValidationError })
  });

  await sendVoiceCall(req, res, mockNext);

  expect(Joi.object).toHaveBeenCalled();
  expect(res.status).toHaveBeenCalledWith(400);
  expect(res.json).toHaveBeenCalledWith({ message: '"phoneNo" must be a valid phone number' });
  expect(userService.getUserByPhone).not.toHaveBeenCalled(); // 校验失败不会调用用户服务
});

关键注意事项

  • 所有需要链式调用的res方法(如status、json)必须用mockReturnThis(),否则后续调用无法被jest捕获。
  • 必须通过jest.mock('joi')把Joi转为mock模块,再修改Joi.object的mock实现,不能直接mockJoi.object()的返回值。
  • 异步函数测试必须用async/await,确保函数执行完成后再做断言。

内容的提问来源于stack exchange,提问作者Abdul-Raffay

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 13:30:30