Author:
bugall
Wechat:bugallF
Email:769088641@qq.com
项目地址: https://github.com/bugall/nod...前端
当咱们在开发前端项目的时候, 不少时候须要根据后端返回的数据来渲染页面, 咱们一般使用AJAX发送请求给服务端。当咱们开发后端逻辑的时候有时候须要链接数据库,根据从数据库中获得的数据来执行后续的逻辑代码, 或者其余的依赖, 甚至会更加复杂棘手。这些开发都存在一个共同的局限性, 就是会去依赖别的服务, 须要别的系统的支持。 例如, 若是咱们使用Ajax请求网络, 您须要有一个服务器来响应对应的请求。对于数据库, 您须要有一个为测试设置的测试数据库。node
全部这些都意味着编写和运行测试更加困难, 由于您须要作额外的工做来准备和设置一个测试成功的环境。
值得庆幸的是, 咱们能够用sinon.js避免全部麻烦。咱们能够利用它的特性将上面的例子简化为几行代码。git
然而, 第一次接触spies
, stub
, mock
可能棘手。它可能很难选择何时用什么功能。它们也有一些问题,因此你须要知道你应该用什么功能解决什么样的问题。
在这篇文章中将向你展现spies
, stub
, mock
什么时候以及如何使用它们,并给你一套最佳实践,帮助您避免常见的陷阱.github
Sinon
具备独立的spies, stub, mock功能,Sinon
并非独立的测试框架,它只是在测试中提供了上述的三种功能, 例如咱们经常使用的测试框架Mocha
,Sinon
并不能彻底替代Mocha
的功能。数据库
Sinon
经过所谓的测试替代(test-double
)轻松消除测试的复杂度,
测试替代,顾名思义,测试中用到的是真实代码逻辑的替代品。回过头来看Ajax示例,咱们不须要设置服务器,而是用Ajax的替代代码,咱们把Ajax的逻辑替换成不须要经过请求服务器就返回预先设置好的数据,这听起来有难以想象,可是基本概念很简单。由于JavaScript是动态的,因此咱们能够在调用某个方法的时候使用任何函数来替换它。在Sinon
中们能够用一个测试逻辑取代任何JavaScript函数,而后让测试复杂的事情变的简单化。后端
顾名思义,spies
咱们干脆就把它称做间谍函数好了,间谍函数是Sinon
最简单的部分,其它的功能都是创建在spies
之上的,spies
的主要用途是收集有关函数调用的信息。您还可使用它们来帮助验证事物,例如是否调用了函数等。就像电影《窃听风云》中同样,监听房间内都有那些人进出,作了什么事,并且这个监听过程是不会房间内的人感知的。一样spies
的实现监听的基础上是不会影响函数自己的正常调用(被监听的函数的上下文关系不会被影响)。固然咱们实现是须要在房间里偷偷的安装窃听器
的, 那么spies
的窃听器是如何实现的呢?后文咱们有介绍服务器
他们拥有spies
的全部功能,不是监视某个函数的调用状况,而是彻底取代了这个函数。换句话说,当使用spies
时,原始函数仍然运行,可是当使用stub
时,函数将不具备原始的功能,而是替换后的函数。网络
mock
与stub
的功能同样都是用来替换指定的函数,若是你想替换掉一个对象中的多个方法,这时mock
就能够发挥做用了,可是若是仅仅是替换对象中的一个函数,那么stub
更加简单易用,当咱们使用mock
的时候应该十分当心,由于大量的替换原有代码逻辑,会致使test变的脆弱
,app
正如名字所暗示的,spies
被用来获取关于函数调用的信息。例如,一个spies
能够告诉咱们调用一个函数的次数、每次调用的参数、返回的值、抛出的错误等。所以,当测试的目的是验证发生的事情时,间谍是一个很好的选择。结合Sinon
的说法,咱们能够经过一个简单的spies
检查不一样的结果。框架
间谍最多见的场景包括:
检查函数被调用了多少次
it('should call save once', function() { var save = sinon.spy(Database, 'save'); setupNewUser({ name: 'test' }, function() { }); save.restore(); sinon.assert.calledOnce(save); });
检查传递给函数的参数
it('should pass object with correct values to save', function() { var save = sinon.spy(Database, 'save'); var info = { name: 'test' }; var expectedUser = { name: info.name, nameLowercase: info.name.toLowerCase() }; setupNewUser(info, function() { }); save.restore(); sinon.assert.calledWith(save, expectedUser); });
存根就像间谍,除了它们替换目标功能。它们还能够包含自定义行为,例如返回值,或抛出异常。他们甚至能够自动调用做为参数提供的任何回调函数。
存根有几个经常使用的用途:
您可使用它们来代替有问题的代码段
您可使用它们来触发不会触发的代码路径,例如错误处理
您可使用它们来帮助测试异步代码更容易
存根可用于替代有问题的代码,即便写入测试困难的代码。这一般是外部网络链接,数据库或其余非JavaScript引发的。这些问题是它们常常须要手动设置。例如,在运行测试以前,咱们须要填写一个带有测试数据的数据库,这使得运行和写入更复杂。
it('should pass object with correct values to save', function() { var save = sinon.stub(Database, 'save'); var info = { name: 'test' }; var expectedUser = { name: info.name, nameLowercase: info.name.toLowerCase() }; setupNewUser(info, function() { }); save.restore(); sinon.assert.calledWith(save, expectedUser); });
经过用stub替换与数据库相关的功能,咱们再也不须要实际的数据库进行测试。 几乎任何状况下,相似的方法均可以用于其余难以测试的代码。
存根也可用于触发不一样的代码路径。 若是咱们测试的代码调用另外一个函数,咱们有时须要测试它在异常条件下的行为, 咱们可使用存根从代码中触发错误:
it('should pass the error into the callback if save fails', function() { var expectedError = new Error('oops'); var save = sinon.stub(Database, 'save'); save.throws(expectedError); var callback = sinon.spy(); setupNewUser({ name: 'foo' }, callback); save.restore(); sinon.assert.calledWith(callback, expectedError); });
主要用于当你存根的时候想验证多个具体的行为时
例如,如下是咱们如何使用mock
验证更具体的数据库保存方案:
it('should pass object with correct values to save only once', function() { var info = { name: 'test' }; var expectedUser = { name: info.name, nameLowercase: info.name.toLowerCase() }; var database = sinon.mock(Database); database.expects('save').once().withArgs(expectedUser); setupNewUser(info, function() { }); database.verify(); database.restore(); });
const sinon = { spyObjs: {}, spy: function(obj, method) { const self = this; this.spyObjs['spy#:' + (Object.keys(self.spyObjs).length + 1)] = {} this.proxy(obj, method); }, proxy: function(obj, method) { const descriptor = Object.getOwnPropertyDescriptor(obj, method); const delegateFlag = 'spy#:' + Object.keys(sinon.spyObjs).length; this.spyObjs[delegateFlag] = { delegateValue: descriptor.value, delegateObject: obj } Object.defineProperty(obj, method, Object.getOwnPropertyDescriptor(this, 'invoke')) }, invoke: function(name) { console.log('参数%s, 被调用了', name) const delegateFlag = 'spy#:' + Object.keys(sinon.spyObjs).length; sinon.spyObjs[delegateFlag].delegateValue.apply(sinon.spyObjs[delegateFlag].delegateObject) } } var testFlag = { sayHello: function(name) { console.log('Hello:%s', name) }, whoAmI: function() { this.sayHello('bugall') console.log('Who am i') } } sinon.spy(testFlag, 'whoAmI'); testFlag.whoAmI('bugall')