JavaScript单元测试中的承诺:权威指南

承诺正在成为JavaScript代码的常见部分。 所有主要的浏览器(包括Chrome,Firefox和Safari)都已经支持本地Promise对象。

尽管使异步代码更简单,但是在单元测试中处理promise还是很麻烦的。 您需要将测试的断言连接到Promise的回调中,这会在测试中添加额外的代码。 这样,测试本身变得有点复杂,并且很难看到正在发生的事情。

在本文中,我将向您展示如何解决此问题,并讨论有用的模式,这些模式可以简化测试阶段的常见承诺方案。

我创建了一个示例项目,您可以从我的网站下载项目,其中显示了本文介绍的技术。

入门

对于这个项目,我将使用Mocha作为测试框架,并使用Chai库提供断言。 一会儿您就会明白为什么。

我们只需运行以下命令即可安装二人组:

npm install mocha chai

当您在单元测试中首次遇到promise时,您的测试可能看起来像典型的单元测试:

var expect = require('chai').expect;

it('should do something with promises', function(done) {
  //define some data to compare against
  var blah = 'foo';

  //call the function we're testing
  var result = systemUnderTest();

  //assertions
  result.then(function(data) {
    expect(data).to.equal(blah);
    done();
  }, function(error) {
    assert.fail(error);
    done();
  });
});

我们有一些测试数据,并调用被测系统-我们正在测试的代码段。 但是随后,promise出现了,代码变得复杂了。

为了实现承诺,我们添加了两个处理程序。 第一个是针对已解决的诺言,在其中具有断言以比较相等性,而第二个是针对被拒绝的诺言,其具有失败的断言。 我们还需要在它们两个中都done()调用。 由于Promise是异步的,因此我们必须告诉Mocha这是一个异步测试,并在完成时通知它。

但是为什么我们需要assert.fail ? 该测试的目的是将成功承诺的结果与价值进行比较。 如果诺言被拒绝,则测试将失败。 这就是为什么如果没有故障处理程序,测试可能会报告误报!

误报是指测试应该失败,但实际上不会失败。 例如,假设我们删除了拒绝回调。 您的代码应如下所示:

result.then(function(data) {
  expect(data).to.equal(blah);
  done();
});

在这种情况下,如果诺言被拒绝,则不会有错误,因为测试中没有错误处理程序来检查它。 但是很明显,在这种情况下测试应该会失败,因为期望不会实现。 这绝对是承诺在测试中变得复杂的主要原因之一。

摩卡和承诺

我决定在此项目中使用Mocha,因为它具有对Promise的内置支持。 这意味着被拒绝的承诺会使您的测试失败。 例如:

it('should fail the test', function() {
  var p = Promise.reject('this promise will always be rejected');
  
  return p;
});

上述测试返回了被拒绝的承诺,这意味着它每次都会失败。 我们可以使用我们学到的知识来改进我们的早期测试,如以下代码片段所示:

var expect = require('chai').expect;

it('should do something with promises', function() {
  var blah = 'foo';

  var result = systemUnderTest();

  return result.then(function(data) {
    expect(data).to.equal(blah);
  });
});

现在,测试将返回承诺。 我们不再需要失败处理程序或done回调,因为Mocha处理了Promise。 如果承诺失败,摩卡咖啡将无法通过测试。

用Chai-promise进一步改进测试

如果我们可以直接在promise上声明,那会不会很好? 凭承诺 ,我们可以!

首先,我们需要在运行时安装它:

npm install chai-as-promised

我们可以这样使用它:

var chai = require('chai');
var expect = chai.expect;

var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);

it('should do something with promises', function() {
  var blah = 'foo';

  var result = systemUnderTest();

  return expect(result).to.eventually.equal(blah);
});

我们已经取代整个then用柴断言设置。 eventually的关键是。 与Chai比较值时,我们可以使用

expect(value).to.equal(something);

但是,如果value是一个承诺,我们eventually插入并返回它:

return expect(value).to.eventually.equal(something)

现在,柴兑现了诺言。

注意:别忘了兑现承诺,否则摩卡咖啡将不知道需要兑现承诺!

我们可以一起使用任何柴氏断言eventually 。 例如:

//assert promise resolves with a number between 1 and 10
return expect(somePromise).to.eventually.be.within(1, 10);

//assert promise resolves to an array with length 2
return expect(somePromise).to.eventually.have.length(2);

测试中承诺的有用模式

比较对象

如果您的诺言的解析值应该是一个对象,则可以像平常一样使用相同的方法进行比较。 例如,使用deep.equal可以编写如下语句:

return expect(value).to.eventually.deep.equal(obj)

同样的警告也适用于没有承诺的情况。 如果要比较对象,则equal将比较引用,并且当对象具有相同的属性但是不同的对象时,测试将失败。

chai-as-promised提供了一个比较对象的便捷助手:

return expect(value).to.eventually.become(obj)

使用eventually.become与进行深层平等比较相同。 除非特别需要引用比较,否则您可以将其用于带有promise的大多数等式比较(包括字符串,数字等)。

从对象主张特定属性

有时,您可能只想根据promise检查对象中的单个属性。 这是一种实现方法:

var value = systemUnderTest();

return value.then(function(obj) {
  expect(obj.someProp).to.equal('something');
});

但是,按照蔡的承诺,有另一种方法。 我们可以利用您可以兑现承诺的事实:

var value = systemUnderTest().then(function(obj) {
  return obj.someProp;
});

return expect(value).to.eventually.equal('something');

作为最后的选择,如果您使用的是ECMAScript 2015,则可以使用胖箭头函数语法使其更加整洁:

var value = systemUnderTest()

return expect(value.then(o => o.someProp)).to.eventually.equal('something');

多重承诺

如果您在测试中有多个承诺,则可以使用Promise.all就像在非测试代码中使用Promise.all一样。

return Promise.all([
  expect(value1).to.become('foo'),
  expect(value2).to.become('bar')
]);

但是请记住,这类似于在单个测试中具有多个断言,这可以看作是代码味道。

比较多个承诺

如果您需要比较两个(或多个)promise,则可以使用以下模式:

return Promise.all([p1, p2]).then(function(values) {
  expect(values[0]).to.equal(values[1]);
});

换句话说,我们可以使用all解决双方的承诺,并使用功能then对返回的值运行正常柴断言。

断言失败

有时您可能想检查某个呼叫是否使诺言失败而不是成功。 在这些情况下,您可以使用chai-as-promised的rejected断言:

return expect(value).to.be.rejected;

如果要确保拒绝伴随特定类型的错误或消息,则还可以使用rejectedWith

//require this promise to be rejected with a TypeError
return expect(value).to.be.rejectedWith(TypeError);

//require this promise to be rejected with message 'holy smokes, Batman!'
return expect(value).to.be.rejectedWith('holy smokes, Batman!');

测试钩

您可以像其他测试功能一样在测试钩子中使用promise。 这适用于beforeafterbeforeEachafterEach 。 例如:

describe('something', function() {
  before(function() {
    return somethingThatReturnsAPromise();
  });

  beforeEach(function() {
    return somethingElseWithPromises();
  });
});

这些工作类似于承诺在测试中的工作方式。 如果诺言被拒绝,摩卡咖啡将抛出一个错误。

承诺和嘲弄/存根

最后,让我们看一下如何在存根中使用promise。 我在下面的示例中使用Sinon.JS。 为此,您需要通过执行以下命令来安装它:

npm install sinon

从存根返回承诺

如果您需要存根或模拟来返回承诺,则答案非常简单:

var stub = sinon.stub();

//return a failing promise
stub.returns(Promise.reject('a failure'));

//or a successful promise
stub.returns(Promise.resolve('a success'));

监视承诺

您可以像其他函数一样将间谍用作promise回调,但是由于promise是异步的,因此可能没有用。 如果您需要对诺言进行断言,则最好使用chai-as-promise。

var spy = sinon.spy();
var promise = systemUnderTest();

promise.then(spy);

希诺承诺

为了稍微简化存根和承诺,我们可以使用约定的sinon。 可以通过npm安装:

npm install sinon-as-promised

它提供了辅助功能来resolvesrejects存根

var sinon = require('sinon');

//this makes sinon-as-promised available in sinon:
require('sinon-as-promised');

var stub = sinon.stub();

//return a failing promise
stub.rejects('a failure');

//or a successful promise
stub.resolves('a success');

结论

Promise可以简化我们的异步代码,甚至可以简化异步测试-只要您向组合中添加一些有用的库即可。

Mocha内置的Promise支持与Chai和chai-as-promised相结合,使测试Promise-turning代码变得简单。 将SinonJS和承诺的sinon添加到混合物中,您也可以轻松地对它们进行存根。

要记住的重要一件事:在测试中使用promise时,请始终从测试中返回一个promise ,否则Mocha不会知道它,并且您的测试可能会在不告知您的情况下无提示地失败。

正如我在简介中提到的那样,我创建了一个示例项目,您可以从我的网站下载示例项目,其中显示了本文介绍的技术。 随时下载并使用它。

From: https://www.sitepoint.com/promises-in-javascript-unit-tests-the-definitive-guide/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值