如何测试Mongoose pre-save钩子中的bcryptjs.hash?单元测试报错TypeError的排查
解决Mongoose模型测试中bcryptjs.hash Stub失败的问题
你遇到的这个错误本质上有两个核心原因:一是你的测试逻辑跳过了Mongoose的模型生命周期钩子,二是可能存在bcryptjs导入不一致的问题,咱们一步步拆解解决:
为什么之前的测试会失败?
- 错误Stub了User.create:你用
sinon.stub(User, 'create').callsFake(() => {return 42})直接替换了create的实现,这等于完全绕过了Mongoose的模型逻辑——pre('save')钩子是在实例保存时触发的,而你自定义的create函数根本不会触发任何模型生命周期,自然不会调用bcryptjs.hash。 - bcryptjs引用不一致:错误提示
Attempted to wrap undefined property hash as function,说明sinon找不到bcryptjs上的hash方法,大概率是你测试文件中导入bcryptjs的方式和User模型文件里的导入方式不匹配(比如模型用默认导入,测试用命名导入)。
修正后的测试代码
下面是能正确测试密码哈希逻辑的代码示例(假设用Mocha+Sinon+Chai栈):
import { expect } from 'chai'; import sinon from 'sinon'; import bcryptjs from 'bcryptjs'; import { User } from '../your-path-to-user-model'; import { arrayOfUsers } from '../your-path-to-test-data'; describe('User Model', () => { // 每次测试后恢复所有stub,避免污染其他测试用例 afterEach(() => { sinon.restore(); }); it('should hash password before saving user', async () => { // 1. Stub bcryptjs.hash,模拟异步哈希返回值 const mockHash = 'fake-hashed-password-123'; const hashStub = sinon.stub(bcryptjs, 'hash').resolves(mockHash); // 2. 创建User实例并调用save,触发pre('save')钩子 const user = new User({ name: arrayOfUsers[0].name, email: arrayOfUsers[0].email, password: arrayOfUsers[0].password }); await user.save(); // 3. 验证哈希逻辑是否正确执行 expect(hashStub.calledOnce).to.be.true; // 检查hash方法的参数是否正确(原始密码 + 盐值rounds=10) expect(hashStub.calledWith(arrayOfUsers[0].password, 10)).to.be.true; // 验证用户密码是否被替换为模拟的哈希值 expect(user.password).to.equal(mockHash); }); // 额外补充:测试isValidPassword方法的逻辑 it('should correctly validate password with isValidPassword', async () => { const compareStub = sinon.stub(bcryptjs, 'compare').resolves(true); const user = new User({ password: 'existing-hashed-password' }); const isValid = await user.isValidPassword('user-input-password'); expect(compareStub.calledWith('user-input-password', 'existing-hashed-password')).to.be.true; expect(isValid).to.be.true; }); });
关键注意事项
- 永远不要跳过模型生命周期:测试Mongoose的钩子逻辑,一定要通过创建实例并调用
save()(或者用User.create但不Stub它)来触发钩子,而不是直接替换掉create的实现。 - 保持依赖导入一致:确保测试文件和模型文件中导入
bcryptjs的方式完全相同(比如都是import bcryptjs from 'bcryptjs',或者都是const bcryptjs = require('bcryptjs'))。 - 清理Stub:用
sinon.restore()在每个测试后恢复被Stub的方法,防止不同测试用例之间互相干扰。
内容的提问来源于stack exchange,提问作者Vladimir Ermolaev
相关产品推荐
相关产品推荐

