无法使用Sinon对含Firebase查询的JavaScript函数进行单元测试
用Sinon模拟Firebase查询测试Promise的解决方案
嘿,我之前也碰到过类似的问题——要测试依赖Firebase的Promise,又不想发起真实请求,关键在于让代码可被模拟,再用Sinon精准mock Firebase的API调用。我给你一步步拆解解决方法:
第一步:重构代码,支持依赖注入
首先得让你的getUserInfo函数能接受一个可选的数据库实例参数,这样测试时就能传入我们的mock对象,而不是真实的Firebase数据库。修改你的pr_services.js:
// services/pr_services.js // 引入真实的Firebase数据库引用 const realDatabase = require('./path-to-your-firebase-db'); exports.getUserInfo = (userId, db = realDatabase) => { return new Promise((resolve, reject) => { const userProfileRef = db.ref('profiles').child(userId); userProfileRef.once('value', (snapshot) => resolve(snapshot.val()), (error) => reject(error) ); }); };
这样做完全不影响生产环境的使用(默认还是用真实数据库),但给测试开了方便之门。
第二步:用Sinon模拟Firebase的链式调用
Firebase的API是链式的(ref().child().once()),所以我们需要一层一层地用Sinon的stub来模拟这些方法,让它们返回下一个方法的mock对象。
创建测试文件test/pr_services.test.js,编写测试用例:
const { expect } = require('chai'); const sinon = require('sinon'); const { getUserInfo } = require('../services/pr_services'); describe('getUserInfo 函数测试', () => { let mockDb; let mockRefStub; let mockChildStub; let mockOnceStub; // 每个测试前初始化mock对象 beforeEach(() => { // 模拟once方法:它接受事件名和回调,我们要能触发这些回调 mockOnceStub = sinon.stub(); // 模拟child方法:返回带有once的对象 mockChildStub = sinon.stub().returns({ once: mockOnceStub }); // 模拟ref方法:返回带有child的对象 mockRefStub = sinon.stub().returns({ child: mockChildStub }); // 最终的mock数据库对象 mockDb = { ref: mockRefStub }; }); // 每个测试后清理stub,避免影响其他测试 afterEach(() => { sinon.restore(); }); it('成功获取用户信息时,应该resolve正确的用户数据', async () => { // 模拟Firebase返回的快照数据 const mockUser = { id: '123', username: 'vinitpradhan18' }; const mockSnapshot = { val: sinon.stub().returns(mockUser) }; // 设置once方法:当调用'value'事件时,触发成功回调 mockOnceStub.withArgs('value').callsArgWith(1, mockSnapshot); // 调用测试函数,传入mock数据库 const result = await getUserInfo('123', mockDb); // 验证调用逻辑是否正确 expect(mockRefStub.calledWith('profiles')).to.be.true; expect(mockChildStub.calledWith('123')).to.be.true; expect(mockOnceStub.calledWith('value')).to.be.true; // 验证返回结果 expect(result).to.deep.equal(mockUser); }); it('Firebase查询失败时,应该reject对应的错误', async () => { const mockError = new Error('无法获取用户信息'); // 设置once方法:触发失败回调 mockOnceStub.withArgs('value').callsArgWith(2, mockError); try { await getUserInfo('456', mockDb); // 如果没抛出错误,测试失败 expect.fail('应该抛出错误'); } catch (error) { // 验证错误是否正确 expect(error).to.equal(mockError); // 验证调用逻辑 expect(mockRefStub.calledWith('profiles')).to.be.true; expect(mockChildStub.calledWith('456')).to.be.true; } }); });
关键知识点解释
- 依赖注入:通过可选参数传入数据库实例,让测试和生产环境的数据源解耦,这是提高代码可测试性的常用技巧。
- 链式调用模拟:因为Firebase的方法是链式调用,所以我们需要从最外层的
ref开始,一层一层stub每个方法的返回值,确保调用链能走通。 - 触发Promise状态:利用Sinon的
callsArgWith方法,手动调用Firebaseonce方法的成功/失败回调,从而让Promise进入resolve或reject状态,完成测试。
如果不想修改原函数的参数,也可以用Sinon直接stub模块中的Firebase数据库引用,但依赖注入的方式更清晰、更灵活,推荐优先使用。
内容的提问来源于stack exchange,提问作者vinitpradhan18
相关产品推荐
相关产品推荐

