如何用Sinon为两个测试用例存根同一方法?及测试覆盖率问题
解决Node+Sinon+Mocha中服务层嵌套方法的测试覆盖问题
看起来你现在卡在了服务层方法的测试覆盖上,尤其是saveLike的嵌套分支和find方法的reject场景没测到。我帮你梳理下问题所在,再给出具体的修复方案。
首先先指出你当前代码里的几个小问题:
- 服务层代码里的
Promises拼写错了,应该是Promise saveLike里的console.log(data)在then((data) => {之前就调用了,会触发未定义错误- 测试用例里把多个测试场景塞到同一个
it里,存根互相干扰,而且异步测试没正确处理(Mocha需要你用done回调或者async/await来等待Promise完成) - 存根没有正确重置,导致不同测试用例之间的存根状态互相影响
一、先修复服务层代码的小bug
先把服务层的代码修正一下,避免测试时因为代码本身的bug失败:
// 修正后的服务层方法 function findLikeByPostIdAndUserId(postId, userId) { return new Promise((resolve, reject) => { likeDislikeDao.findLikeByPostIdAndUserId(postId, userId) .then((data) => resolve(data)) .catch((error) => reject(error)); }); } function findDislikeByPostIdAndUserId(postId, userId) { return new Promise((resolve, reject) => { likeDislikeDao.findDislikeByPostIdAndUserId(postId, userId) .then((data) => resolve(data)) .catch((error) => reject(error)); }); } function saveLike(like) { console.log(like); return new Promise((resolve, reject) => { likeDislikeDao.findLikeByPostIdAndUserId(like.postId, like.userId) .then((likeData) => { if (!likeData) { likeDislikeDao.findDislikeByPostIdAndUserId(like.postId, like.userId) .then((dislikeData) => { if (!dislikeData) { likeDislikeDao.saveLike(like) .then((data) => resolve(data)); } else { likeDislikeDao.deleteDislike(dislikeData._id) .then(() => likeDislikeDao.saveLike(like)) .then((data) => resolve(data)); } }); } else { likeDislikeDao.deleteLike(likeData._id) .then((data) => resolve(data)); } }) .catch((error) => reject(error)); }); }
二、优化测试用例,覆盖所有分支
1. 使用Sinon沙箱管理存根
用sinon.createSandbox()可以自动管理所有存根,测试结束后一键恢复,避免存根污染其他测试用例。
2. 每个测试场景单独写it用例
把saveLike的每个分支、find方法的resolve和reject都拆成独立的测试用例,逻辑更清晰,也方便单独验证每个分支。
3. 用async/await简化异步测试
这样不用嵌套then/catch,代码更易读,也能确保Mocha等待异步操作完成,不会出现测试提前结束的问题。
修改后的测试用例:
const sinon = require('sinon'); const assert = require('assert'); const likeDislikeService = require('./your-service-path'); // 替换为你的服务文件路径 const likeDislikeDao = require('./your-dao-path'); // 替换为你的DAO文件路径 describe('LikeDislike Service', function () { let sandbox; // 每个测试前创建沙箱 beforeEach(function () { sandbox = sinon.createSandbox(); }); // 每个测试后恢复所有存根 afterEach(function () { sandbox.restore(); }); // 测试findLikeByPostIdAndUserId的所有分支 describe('findLikeByPostIdAndUserId', function () { it('should return like data when found', async function () { // 存根DAO方法返回成功数据 const mockLike = [{ _id: '1', postId: 1, userId: 2 }]; sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId') .resolves(mockLike); const result = await likeDislikeService.findLikeByPostIdAndUserId(1, 2); assert.strictEqual(result.length, 1); }); // 测试reject分支 it('should reject when DAO throws error', async function () { const mockError = new Error('DB query failed'); sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId') .rejects(mockError); try { await likeDislikeService.findLikeByPostIdAndUserId(1, 2); // 如果走到这里说明没reject,断言失败 assert.fail('Expected promise to reject'); } catch (err) { assert.strictEqual(err, mockError); } }); }); // 测试findDislikeByPostIdAndUserId的所有分支 describe('findDislikeByPostIdAndUserId', function () { it('should return dislike data when found', async function () { const mockDislike = [{ _id: '2', postId: 1, userId: 2 }]; sandbox.stub(likeDislikeDao, 'findDislikeByPostIdAndUserId') .resolves(mockDislike); const result = await likeDislikeService.findDislikeByPostIdAndUserId(1, 2); assert.strictEqual(result.length, 1); }); it('should reject when DAO throws error', async function () { const mockError = new Error('DB query failed'); sandbox.stub(likeDislikeDao, 'findDislikeByPostIdAndUserId') .rejects(mockError); try { await likeDislikeService.findDislikeByPostIdAndUserId(1, 2); assert.fail('Expected promise to reject'); } catch (err) { assert.strictEqual(err, mockError); } }); }); // 测试saveLike的所有嵌套分支 describe('saveLike', function () { const testLike = { postId: 1, userId: 2 }; const mockLikeData = { _id: 'like-1', ...testLike }; const mockDislikeData = { _id: 'dislike-1', postId: 1, userId: 2 }; // 场景1:用户既没点赞也没点踩,直接保存点赞 it('should save like when no existing like or dislike', async function () { sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId').resolves(null); sandbox.stub(likeDislikeDao, 'findDislikeByPostIdAndUserId').resolves(null); sandbox.stub(likeDislikeDao, 'saveLike').resolves(mockLikeData); const result = await likeDislikeService.saveLike(testLike); assert.deepStrictEqual(result, mockLikeData); // 验证存根被正确调用 assert.ok(likeDislikeDao.saveLike.calledOnceWith(testLike)); }); // 场景2:用户有点踩,先删除踩再保存赞 it('should delete existing dislike then save like', async function () { sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId').resolves(null); sandbox.stub(likeDislikeDao, 'findDislikeByPostIdAndUserId').resolves(mockDislikeData); sandbox.stub(likeDislikeDao, 'deleteDislike').resolves({ deletedCount: 1 }); sandbox.stub(likeDislikeDao, 'saveLike').resolves(mockLikeData); const result = await likeDislikeService.saveLike(testLike); assert.deepStrictEqual(result, mockLikeData); assert.ok(likeDislikeDao.deleteDislike.calledOnceWith(mockDislikeData._id)); assert.ok(likeDislikeDao.saveLike.calledOnceWith(testLike)); }); // 场景3:用户有点赞,删除现有点赞(不保存新赞) it('should delete existing like when user already liked', async function () { sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId').resolves(mockLikeData); sandbox.stub(likeDislikeDao, 'deleteLike').resolves({ deletedCount: 1 }); const result = await likeDislikeService.saveLike(testLike); assert.deepStrictEqual(result, { deletedCount: 1 }); assert.ok(likeDislikeDao.deleteLike.calledOnceWith(mockLikeData._id)); // 验证saveLike没有被调用 assert.ok(likeDislikeDao.saveLike.notCalled); }); // 场景4:DAO层findLike方法抛出错误,测试reject分支 it('should reject when findLike throws error', async function () { const mockError = new Error('DB query failed'); sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId').rejects(mockError); try { await likeDislikeService.saveLike(testLike); assert.fail('Expected promise to reject'); } catch (err) { assert.strictEqual(err, mockError); } }); // 场景5:DAO层saveLike抛出错误,测试reject分支 it('should reject when saveLike throws error', async function () { const mockError = new Error('Save failed'); sandbox.stub(likeDislikeDao, 'findLikeByPostIdAndUserId').resolves(null); sandbox.stub(likeDislikeDao, 'findDislikeByPostIdAndUserId').resolves(null); sandbox.stub(likeDislikeDao, 'saveLike').rejects(mockError); try { await likeDislikeService.saveLike(testLike); assert.fail('Expected promise to reject'); } catch (err) { assert.strictEqual(err, mockError); } }); }); });
关键要点总结
- 隔离测试场景:每个
it只测一个分支,避免存根互相干扰 - 沙箱管理存根:用Sinon沙箱自动恢复存根,不用手动逐个重置
- 异步测试用async/await:比then/catch更清晰,也能确保Mocha等待异步操作完成
- 验证存根调用:用
calledOnce、calledWith等断言存根是否被正确调用,确保代码逻辑符合预期 - 覆盖所有分支:包括正常流程、异常流程(reject)、各种嵌套条件分支
内容的提问来源于stack exchange,提问作者joler-botol
相关产品推荐
相关产品推荐

