如何对一个调用另一个返回promise的函数进行单元测试?

时间:2016-08-27 11:13:47

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

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

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

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

因此,对于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();
});

当我测试的函数返回自己的一个承诺时,我已经测试了我的类似场景,因为我可以在当时挂钩我的断言。

我也尝试用控制器包装controller.findAll并从response.send解析它,但它也没有用。

3 个答案:

答案 0 :(得分:6)

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

答案 1 :(得分:1)

这里的想法是让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; }); (希望你的节点版本不是太老了)。

          <form action="add_sales.php" method="POST">
        <table class="table table-striped table-bordered table-hover results table-fixed table-condensed">
          <thead>
            <tr>
                <th class="text-center">#</th>
                <th>Product Name</th>
                <th>Description</th>
                <th>Price</th>
                <th>In Stock</th>
                <th style="width: 20%">Quantity</th>
            </tr>
            <tr class="warning no-result">
              <td colspan="8"><i class="fa fa-warning"></i> No Product Found</td>
            </tr>
          </thead>


          <tbody>
          <?php 
            $query = "SELECT * FROM products";
            $exec = mysqli_query($connection, $query);
            $a = 1;
            $b = 1;

            while ($row = mysqli_fetch_array($exec)) {

              $product_id = $row['product_id'];
              $product_name = $row['product_name'];
              $product_price = $row['sell_price'];
              $description = $row['description'];
              $product_quantity = $row['quantity'];

           ?>
          <tr>
          <td class="text-center"><?php echo $product_id; ?>
            <input type="hidden" name="product_id[]" value="<?php echo $product_id; ?>">
          </td>
              <td><?php echo $product_name; ?></td>
              <td><?php echo $description; ?></td>
              <td><?php echo $product_price; ?></td>
              <td><input type="number" name="hehe" value="<?php echo $product_quantity; ?>" id="<?php echo "qtyResult" . $a++; ?>" disabled></td>
              <td><input type="number" name="qtyBuy[]" id="<?php echo "qtyBuy" . $b++; ?>" onkeyup="updateStock(this, event)"></td>
          </tr>
          <?php } ?>
          </tbody>
      </table>


      </div>
      <div class="form-group">
          <input type="submit" name="addCart" value="Add Items to Cart" class="btn btn-info pull-right">

      </div>
      </form>

答案 2 :(得分:1)

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

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

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

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

执行此操作的一种简单方法是导出函数contstructor而不是对象。

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方法。完全不需要进行异步测试。