如何用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); } };
错误原因分析
- res.status未被调用:模拟
res对象时未处理链式调用(比如res.status()没返回res本身),或函数执行到异常分支但未正确捕获。 - 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
相关产品推荐
相关产品推荐

