登录控制器功能正常但数据库异常场景单元测试失败
登录控制器数据库访问失败场景单元测试失败排查
负责的登录控制器功能正常,但验证数据库访问失败时返回500状态码的单元测试始终失败。已用sinon包mock mongoose的findOne方法模拟数据库失败场景,但测试报错。
测试代码:
describe("Auth Controller - Login", function () { it("should throw an error with code 500 if accessing the database fails", function (done) { sinon.stub(User, "findOne"); User.findOne.throws(); const req = { body: { email: "test@test.com", password: "test123", }, }; authController .postLogin(req, {}, () => {}) .then((result) => { expect(result).to.be.an("error"); expect(result).to.have.property("httpStatusCode", 500); done(); }); User.findOne.restore(); }); });
控制器代码:
exports.postLogin = (req, res, next) => { const email = req.body.email; const password = req.body.password; const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ error: errors.array()[0].msg }); } User.findOne({ email: email }) .then((user) => { if (!user) { return res.status(401).json({ error: "User does not exist!" }); } bcrypt .compare(password, user.password) .then((doMatch) => { if (doMatch) { const token = jwt.sign( { email: email, userId: user._id }, process.env.SESSION_SECRET, { expiresIn: "3d", } ); return res.status(200).json({ token: token, userId: user._id.toString(), email: email, }); } return res.status(401).json({ error: "Invalid Password!" }); }) .catch((err) => { throw new Error(err); }); }) .catch((err) => { const error = new Error(err); error.httpStatusCode = 500; next(error); return error; }); };
测试报错信息:
1 failing 1) Auth Controller - Login should throw an error with code 500 if accessing the database fails: Error Error at Object.fake.exceptionCreator (node_modules/sinon/lib/sinon/default-behaviors.js:24:20) at Object.invoke (node_modules/sinon/lib/sinon/behavior.js:165:35) at Function.functionStub (node_modules/sinon/lib/sinon/stub.js:42:43) at Function.invoke (node_modules/sinon/lib/sinon/proxy-invoke.js:50:47) at Function.findOne (node_modules/sinon/lib/sinon/proxy.js:285:26) at Object.exports.postLogin (controllers/auth.js:68:7) at Context.<anonymous> (test/auth-controller.js:19:5) at processImmediate (internal/timers.js:464:21)
问题原因及修复方案
1. 控制器未返回完整Promise链
postLogin函数没有返回User.findOne()启动的Promise链,导致测试中调用.then()时因undefined无该方法报错,同时catch块返回的error也无法被测试捕获。
修改控制器,返回完整Promise链:
exports.postLogin = (req, res, next) => { const email = req.body.email; const password = req.body.password; const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ error: errors.array()[0].msg }); } // 返回Promise链,让测试可捕获后续结果 return User.findOne({ email: email }) .then((user) => { if (!user) { return res.status(401).json({ error: "User does not exist!" }); } // 返回bcrypt的Promise链,避免嵌套导致的未捕获错误 return bcrypt .compare(password, user.password) .then((doMatch) => { if (doMatch) { const token = jwt.sign( { email: email, userId: user._id }, process.env.SESSION_SECRET, { expiresIn: "3d", } ); return res.status(200).json({ token: token, userId: user._id.toString(), email: email, }); } return res.status(401).json({ error: "Invalid Password!" }); }) .catch((err) => { const error = new Error(err); error.httpStatusCode = 500; throw error; // 抛出错误让外层catch统一处理 }); }) .catch((err) => { const error = new Error(err); error.httpStatusCode = 500; next(error); return error; // 返回错误给测试用例 }); };
2. Sinon Stub恢复时机错误
测试中User.findOne.restore()放在.then()之前,会导致stub在postLogin调用前就被恢复,findOne不会抛出预期错误。需将恢复操作放在测试完成后的回调中:
修改测试代码:
describe("Auth Controller - Login", function () { it("should throw an error with code 500 if accessing the database fails", function (done) { const findOneStub = sinon.stub(User, "findOne"); findOneStub.throws(new Error("Database error")); // Mock validationResult,避免验证逻辑干扰数据库失败场景测试 const validationStub = sinon.stub(require('express-validator'), 'validationResult') .returns({ isEmpty: () => true }); const req = { body: { email: "test@test.com", password: "test123", }, }; authController .postLogin(req, {}, (err) => { // 同时通过next回调验证错误传递 expect(err).to.have.property("httpStatusCode", 500); }) .then((result) => { expect(result).to.be.an("error"); expect(result).to.have.property("httpStatusCode", 500); // 测试完成后恢复stub findOneStub.restore(); validationStub.restore(); done(); }) .catch((err) => { // 捕获测试异常,避免done未调用 findOneStub.restore(); validationStub.restore(); done(err); }); }); });
3. 未Mock validationResult
测试中未mockvalidationResult(req),会导致errors.isEmpty()可能返回false,提前返回422状态码,无法进入数据库查询逻辑。需mock该方法返回空错误结果。
内容的提问来源于stack exchange,提问作者Sai Krishnadas
相关产品推荐
相关产品推荐

