如何对调用另一个 returns 承诺的函数进行单元测试?

How to unit test a function which calls another that returns a promise?

我有一个使用 express 4 的 node.js 应用,这是我的控制器:

var service = require('./category.service');

module.exports = {
  findAll: (request, response) => {
    service.findAll().then((categories) => {
      response.status(200).send(categories);
    }, (error) => {
      response.status(error.statusCode || 500).json(error);
    });
  }
};

它称我的服务为 returns 承诺。一切正常,但我在尝试对其进行单元测试时遇到了麻烦。

基本上,我想确保根据我的服务 returns,我使用正确的状态代码和正文刷新响应。

所以对于 mocha 和 sinon,它看起来像:

it('Should call service to find all the categories', (done) => {
    // Arrange
    var expectedCategories = ['foo', 'bar'];

    var findAllStub = sandbox.stub(service, 'findAll');
    findAllStub.resolves(expectedCategories);

    var response = {
       status: () => { return response; },
       send: () => {}
    };
    sandbox.spy(response, 'status');
    sandbox.spy(response, 'send');

    // Act
    controller.findAll({}, response);

    // Assert
    expect(findAllStub.called).to.be.ok;
    expect(findAllStub.callCount).to.equal(1);
    expect(response.status).to.be.calledWith(200); // not working
    expect(response.send).to.be.called; // not working
    done();
});

当我正在测试的函数 returns 本身就是一个承诺时,我已经测试了类似的场景,因为我可以在那时挂钩我的断言。

我也试过用 Promise 包装 controller.findAll 并从 response.send 解决它,但它也没有用。

这里的想法是让 service.findAll() returns 可以在测试代码中访问而无需调用 service。据我所知,您可能使用的 sinon-as-promised 不允许这样做。所以我只使用了原生的 Promise(希望你的节点版本不会太旧)。

const aPromise = Promise.resolve(expectedCategories); 
var findAllStub = sandbox.stub(service, 'findAll');
findAllStub.returns(aPromise);

// response = { .... }

controller.findAll({}, response);

aPromise.then(() => {
    expect(response.status).to.be.calledWith(200);
    expect(response.send).to.be.called;    
});

您应该将断言部分移动到 res.send 方法中,以确保在断言之前完成所有异步任务:

var response = {
   status: () => { return response; },
   send: () => {
     try {
       // Assert
       expect(findAllStub.called).to.be.ok;
       expect(findAllStub.callCount).to.equal(1);
       expect(response.status).to.be.calledWith(200); // not working
       // expect(response.send).to.be.called; // not needed anymore
       done();
     } catch (err) {
       done(err);
     }
   },
};

当代码难以测试时,它可能表明可以探索不同的设计可能性,从而促进轻松测试。跳出来的是 service 包含在您的模块中,并且根本没有暴露依赖关系。我觉得目标不应该是找到一种方法来测试你的代码,而是找到一个最优的设计。

IMO 目标是找到一种公开 service 的方法,以便您的测试可以提供存根实现,从而可以隔离、同步地测试 findAll 的逻辑。

一种方法是使用像 mockeryrewire 这样的库。两者都相当容易使用,(根据我的经验,随着测试套件和模块数量的增长,嘲笑开始退化并变得非常难以维护)它们将允许您通过提供自己的服务对象来修补 var service = require('./category.service');它自己的 findAll 定义。

另一种方法是重新设计您的代码,以某种方式将 service 公开给调用者。这将允许您的调用者(单元测试)提供自己的 service 存根。

一个简单的方法是导出函数构造函数而不是对象。

module.exports = (userService) => {

  // default to the required service
  this.service = userService || service;

  this.findAll = (request, response) => {
    this.service.findAll().then((categories) => {
      response.status(200).send(categories);
    }, (error) => {
      response.status(error.statusCode || 500).json(error);
    });
  }
};

var ServiceConstructor = require('yourmodule');
var service = new ServiceConstructor();

现在测试可以为 service 创建存根并将其提供给 ServiceConstructor 以执行 findAll 方法。完全不需要异步测试。