【问题标题】:How to unit test a function which calls another that returns a promise?如何对调用另一个返回承诺的函数进行单元测试?
【发布时间】:2017-01-03 23:08:32
【问题描述】:

我有一个使用 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);
    });
  }
};

它调用我的服务返回一个承诺。一切正常,但我在尝试对其进行单元测试时遇到了麻烦。

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

所以对于 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();
});

当我正在测试的函数返回一个 Promise 时,我已经测试了我的类似场景,因为我可以在 then 中挂钩我的断言。

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

【问题讨论】:

  • 每个调用 promise 返回函数的函数都是异步的,并且应该返回一个 promise 本身。如果你只接受回调,那么你必须回退到测试基于回调的 API。
  • 你在用 Chai 吗?如果是这样,chaijs.com/plugins/chai-as-promised
  • 是的,但是我正在测试的函数没有返回一个承诺子链,正如承诺的那样无济于事

标签: javascript node.js unit-testing promise sinon


【解决方案1】:

您应该将您的断言部分移动到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);
     }
   },
};

【讨论】:

  • 是的,现在可以了。但是,如果我尝试测试失败(例如,将状态与 204 进行比较),它不会显示失败的断言。它只是超时并说:Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
  • @jbernal 我的错,忘了你必须捕获抛出的错误并将其传递给done,请参阅编辑后的答案
  • 非常感谢。使用 try/catch 块救了我。
【解决方案2】:

这里的想法是让service.findAll() 返回的承诺可以在测试代码中访问,而无需调用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;    
});

【讨论】:

  • 我知道这种情况。但如果你意识到,我的controller.findAll 没有返回承诺,它不需要它。因此,我的问题。我无法在其中执行 then。
  • @jbernal,我明白你的意思。我混淆了findAlls。如果您拨打controller.findAll() 并在该链之后拨打service.findAll(),即constoller.findAll(); service.findAll().then(() => { expect(....) });
  • 不这么认为...controller.findAll 是调用service.findAll 的那个,这是正在测试的东西之一,所以我不想在其中明确调用它。
  • @jbernal,请在答案中查看更正版本。我删除了我的初始代码,因为它没有回答任何问题。
  • @jbernal,我没有看到您现在拥有的实际代码,但这可能是因为 Mocha 在将其用作测试的返回值时等待解决承诺。要测试错误案例,您仍然必须使用done(),即aPromise.catch(() => { /* assertions */; done() })
【解决方案3】:

当代码难以测试时,它可能表明可能有不同的设计可能性可供探索,从而促进轻松测试。跳出来的是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 方法。完全不需要异步测试。

【讨论】:

  • 非常感谢您的回答。这些绝对是我需要研究的想法。我正在开发我的第一个节点应用程序,我确信我可以遵循更多优化和更好的结构化模式来改进我的代码。我将尝试实施它们。干杯
猜你喜欢
  • 2017-05-08
  • 2021-09-07
  • 2018-01-21
  • 2016-10-04
  • 2021-07-23
  • 1970-01-01
  • 2017-05-26
  • 2018-07-27
  • 2017-05-16
相关资源
最近更新 更多