承诺正在成为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。 这适用于before
, after
, beforeEach
和afterEach
。 例如:
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
它提供了辅助功能来resolves
和rejects
存根
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/