You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:19:33