sinon.js基础使用教程---单元测试

简介: 在真实的项目中,我们的代码经常要做各种导致我们测试很难进行的事情。Ajax请求,timer,日期,跨浏览器特性…或者如果你使用Nodejs,则面对数据库,网络,文件操作等。

译文

当我们写单元测试时一个最大的绊脚石是当你面对的代码过于复杂。

在真实的项目中,我们的代码经常要做各种导致我们测试很难进行的事情。Ajax请求,timer,日期,跨浏览器特性…或者如果你使用Nodejs,则面对数据库,网络,文件操作等。

所有这些事情之所以不容易测试是因为你无法轻易用代码控制它们。如果你使用Ajax,你需要一个服务端来响应请求,这样才能让你的测试项通过。如果你使用setTimeout,你的测试项不得不等待它。如果是数据库或网络,也类似–你需要一个包含正确数据的数据库,或一个网络服务。

真实世界不像那些测试教程里看起来的那样简单。但你知道有一个解决方案么?

By using Sinon, we can make testing non-trivial code trivial! (译者:这个口号不太好翻译,non-trivial)

让我们看看该怎么做。

是什么让Sinon如此重要?

简单的说,Sinon允许你去替换代码中复杂的部分,以此来简化你的测试代码。

当我们测试某部分代码时,你不希望受到其它部分的影响。如果有外部因素影响测试,那么测试项将变得非常复杂且不稳定。

如果你想测试一个使用了ajax的代码,你该怎么做?你需要跑一个服务端,并保证该服务端返回指定的响应数据来支撑你的测试项。这很难完成也让运行测试很麻烦。

那如果你的代码依赖时间呢?假如它需要等待一秒钟才执行。怎么办?你需要在你的测试项中使用setTimeout,但这会让测试变得缓慢。想像一下,如果间隔时间很久,例如五分钟。我想你不会希望每次跑测试项都等待五分钟吧。

如果使用Sinon,我们可以搞定这些问题(甚至更多),并减少复杂度。

Sinon是怎么工作的?


Sinon通过允许我们简单的创建test-doubles从而帮助我们减少测试项编写的复杂度。

正如它名字一样,Test-doubles作用是在测试中替换某部分代码。上面提到的ajax的例子中,不需要创建服务端,我们可以使用test-doubles替换掉Ajax调用。在timer例子中,我们可以使用test-doubles来控制时间。

听起来可能很复杂,但基本思想很简单。基于javascript的动态性,我们可以替换任何函数。Test-doubles只是在这个思想的基础上走的更远了一些。使用Sinon,我们可以使用test-doubles替换任何javascript函数,并提供很多方便测试的配置。

Sinon中test-doubles分三类:

  • Spies,提供了函数调用的信息,但不会改变其行为(译者注:类似动态代理)
  • Stubs,类似Spies,但是是完全替换目标函数。这可以让你随心所欲的控制函数–抛异常,返回指定结果等
  • Mocks,提供了替换整个对象的能力

此外,Sinon还提供了其他的辅助功能,本文不包含下面的范围:

基于这些功能,Sinon可以让你解决测试中遇到的由外部依赖带来的所有复杂问题。如果你学会了Sinon提供的这些技巧,你几乎不需要其它别的工具了。

安装Sinon

开始之前,我们需要安装Sinon

Nodejs

  1. 使用npm install sinon安装sinon
  2. 在测试项中引入sinon:var sinon = require('sinon');

浏览器

  1. 你可以选择npm install sinon,或使用CDN,也可以从官网下载到本地
  2. 在你的测试页面引入sinon.js

入门指南

sinon包含许多功能,但它们多数都存在关系。你只需要掌握一部分,就会了解剩余部分。这让sinon很容易使用,只需要你了解了基本用法并知道它们之间的差别。

只要我们的代码调用了一个不容易控制的函数,我们通常就需要sinon。

对于Ajax,它可能是$.get或者XMLHttpResquest。对于timer,它可能是setTimeout。对于数据库,它可能是mongodb.findOne

为了方便我们讨论,后面我将成这类函数为依赖方。我们测试的目标函数依赖其它函数的返回结果。

最常见的使用sinon方式是使用test-doubles替换掉问题依赖方

  • 当测试Ajax时,我们使用test-doubles替换XMLHttpResquest来伪造ajax请求
  • 当测试timer时,我们伪造替换setTimeout
  • 当测试数据库时,我们使用test-doubles来替换mongodb.findOne来直接返回伪造数据

让我们写点代码吧。

Spies

Spies很简单,但其它很多功能依赖它。

spies的主要用法是收集函数的调用信息。你可以用来验证一些事儿,例如函数是否被调用。



var spy = sinon.spy();

//我们可以像调用函数一样调用spy
spy('Hello', 'World');

//我们可以得到调用信息
console.log(spy.firstCall.args); //output: ['Hello', 'World']


sinon.spy函数返回一个Spy对象,该对象可以像函数一样被调用,它记录每次被调用信息。在上面的例子里,firstCall属性包含了第一次调用的信息,例如firstCall.args表示调用时的参数列表。

虽然你可以像上面例子那样创建一个匿名spies,但通常情况下你需要使用spy替换一个其它函数。



var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为user.setName创建一个spy
var setNameSpy = sinon.spy(user, 'setName');

//现在,每次调用目标函数,spy都会记录相关信息
user.setName('Darth Vader');

//我们可以使用spy对象查看相关信息
console.log(setNameSpy.callCount); //output: 1

//非常重要的步骤--拆除spy
setNameSpy.restore();

上面例子展示了使用spy替换其它函数的写法,最重要的一点是:当你确定不再需要spy后,你记得恢复原始函数,参考例子中的最后一行。不然测试可能出现非预期行为。

Spies包含许多不同的属性,用来提供不同的信息。spy文档列出了完整的属性列表。

在实际场景中,你可能不会经常使用spies。你更多时候使用的是stub,但是spies用来检测函数是否被调用非常方便:



function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});

在这个例子中,我们使用Mocha作为测试框架,使用Chai作为断言库。如果你想了解更多信息,可以参考我之前的文章:使用Mocha和Chai来单元测试你的javascript

See the Pen Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

sinons断言

在我们介绍stubs之前,我们快速看一下sinon断言

大多数使用spies(和stubs)的测试方案中,你需要一些工具来校验测试结论。

我们可以使用任何断言来验证结论。前面的例子中,我们使用Chai的assert函数来验证值的真实性。



assert(callback.calledOnce);

这样做的问题是错误信息并不清晰。你将得到“false was not true”,或类似信息。你可以想象的到,这对于定位错误并不是很有价值,你需要在测试代码中翻找才能最终找到。一点都不美。

解决这个问题,我们可恶意包含一个自定义的错误信息在断言中。


assert(callback.calledOnce, 'Callback was not called once');

但如果我们使用sinon的断言库呢?


describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    sinon.assert.calledOnce(callback);
  });
});

使用sinon断言我们可以得到更多有价值的错误信息。这在当你验证比较复杂的条件时非常有用,例如函数的参数。

下面列出一些sinon提供的其它强大断言的一些例子:

  • sinon.assert.calledWith可以用来验证函数是否使用指定的参数(这可能是我用的最多的一个)
  • sinon.assert.callOrder用来验证函数的调用顺序

sinon断言文档介绍了所有的内容。如果你喜欢使用Chai,有一个sinon-chai-plugin可以让你通过chai的expectshould接口来使用sinon断言。

Stubs

stubs归类于test-doubles是因为它的灵活和方便性。它拥有spies的全部功能,此外它还彻底的替换掉了目标函数。换句话说,当你使用spy,原始的函数依然会被调用,但如果使用stub,原始函数就不会被执行了。

这个特性让stub可以胜任许多任务,例如:

  • 替换像ajax或其它外部函数等让测试变复杂或慢的调用
  • 根据函数的响应来触发不同的代码流程
  • 测试不寻常的条件,如抛出异常

我们可以像创建spies一样创建stubs:



var stub = sinon.stub();

stub('hello');

console.log(stub.firstCall.args); //output: ['hello']

我们创建了一个匿名的stubs,但用stubs来替换存在的函数更有意义。

举个例子,如果你有一段代码调用了jquery的Ajax,测试它将变得麻烦。代码会发送请求到我们配置的服务端,所以我们需要保证服务端的有效性,或者给代码添加特定的分支来适配测试环境 – 这么做真的大错特错。你不应该在代码中编写任何测试特定逻辑。

我们可以使用sinon的stub来替换ajax调用。这会让测试变得简单。

下面的例子中,我们使用ajax向预定url发送一个携带参数的请求。



function saveUser(user, callback) {
  $.post('/users', {
    first: user.firstname,
    last: user.lastname
  }, callback);
}

通常,测试这个函数将变的很麻烦,但我们有了stub,一切变得美好。

假如我们想要确保传递给saveUser函数的回调方法在请求结束后正确的被执行了一次。


describe('saveUser', function() {
  it('should call callback after saving', function() {

    //We'll stub $.post so a request is not sent
    var post = sinon.stub($, 'post');
    post.yields();

    //We can use a spy as the callback so it's easy to verify
    var callback = sinon.spy();

    saveUser({ firstname: 'Han', lastname: 'Solo' }, callback);

    post.restore();
    sinon.assert.calledOnce(callback);
  });
});

See the Pen Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

这里,我们将ajax函数替换成了stub。这意味着请求不会被发送,我们不需要一个服务端 – 我们全权控制了我们的测试代码!

介于我们想确认我们传给saveUser的回调会被执行,我们让stub立刻返回。这意味着stub将自动调用callback参数。这模仿了$.post在请求完成后的行为。

除了stub,我们还创建了一个spy。我们可以使用一个普通的函数作为回调,但使用spy会让sinon.assert.calledOnce更方便验证测试结论。

大多数需要stub的场景,都类似下面步骤:

  • 确认是否包含问题函数,例如$.post
  • 观察并掌握其行为
  • 创建一个stub
  • 让stub来模拟目标行为

stub不需要模拟所有的行为,只需要足够你的测试项使用即可,其它细节可以忽略。

另外一些stub的常用场景是验证一个函数是否使用特定的参数。

举个例子,在我们的ajax函数中,我们希望确定正确的数据被提交。因此,我们可能会这么做:


describe('saveUser', function() {
  it('should send correct parameters to the expected URL', function() {

    //We'll stub $.post same as before
    var post = sinon.stub($, 'post');

    //We'll set up some variables to contain the expected results
    var expectedUrl = '/users';
    var expectedParams = {
      first: 'Expected first name',
      last: 'Expected last name'
    };

    //We can also set up the user we'll save based on the expected data
    var user = {
      firstname: expectedParams.first,
      lastname: expectedParams.last
    }

    saveUser(user, function(){} );
    post.restore();

    sinon.assert.calledWith(post, expectedUrl, expectedParams);
  });
});

see the pen Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

这次,我们有创建了一个$.post()的stub,但这回我们并没有让它直接返回。这次我们的测试目标不是回调,因此让它返回并不是必须的。

我们设置了一些变量来存期望的数据 - url和参数。这是一个好的实践,让我们很容易知道什么是测试必须的。也可以帮助我们减少重复代码。

这次我们使用sinon.assert.calledWith()断言。我们将stub传递进去,因为我们想确定stub包含了正确的参数。

使用sinon,还有其它的方法来测试ajax请求。例如使用sinon的伪造XMLHttpResquest功能。我们不会在这里去介绍细节,如果你想了解更多可以参考my article on Ajax testing with Sinon’s fake XMLHttpRequest

Mocks

Mocks不同于stubs。如果你之前听过mock object这个术语,那没错了 - sinon的mocks用来替换整个对象,并改变其行为。

如果你需要替换某个对象的多个方法,你就应该使用mocks。如果你只是希望替换某个单独的方法,stub更方便。

使用mocks时你需要小心!因为它太TM强大了,很容易让你的测试过于特定 - 测试的太细或太刻意 - 从而让你的测试太容易过期。

与spies和stubs不同,mocks包含内建的断言。当使用mock对象时,你可以定义你期望的结果,你期望的行为。

假设我们使用store.js来保存一些数据到localstorage,我们打算测试这个特性。我们可以使用mock来写测试:



describe('incrementStoredData', function() {
  it('should increment stored value by one', function() {
    var storeMock = sinon.mock(store);
    storeMock.expects('get').withArgs('data').returns(0);
    storeMock.expects('set').once().withArgs('data', 1);

    incrementStoredData();

    storeMock.restore();
    storeMock.verify();
  });
});

See the Pen Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

使用mocks时,我们可以使用链式调用风格来定义期望的调用和结果。这和使用断言验证结果一样,除了我们需要提前定义,并在测试结束时校验它们storeMock.verify()

调用mock对象的mock.expects(something)会创建一个期望值。意味着mock.something()方法期望被调用。Each expectation, in addition to mock-specific functionality, supports the same functions as spies and stubs.(译者注:只能意会无法言表啊)

你可能会觉得通常stub都比mock更简单 - 没错。Mocks要小心使用。

mock特定的特性,可以查看sinon的mock文档

重要的最佳实践:使用sinon.test()

这里有个使用sion的很重要的最佳实践,不管是使用spies,stubs还是mocks都应该牢记。

如果你用test-doubles替换了一个存在的函数,则使用sinon.test()

前面的例子中,我们使用stub.restore()mock.restore()来在我们使用完后清理它们。这很有必要,否则test-doubles将持续有效,这将可能影响其他的测试项并导致错误。

但是,直接使用restore()可能很难,有可能因为某个异常导致restore()没有被调用!

我们有两种方法来解决这个问题:我们可以自己包装完整的try catch块。这允许我们将restore()放在finally块中调用来确保一切正常。

或者,一个更好的做法是我们可以将测试体写在sinon.test()中:



it('should do something with stubs', sinon.test(function() {
  var stub = this.stub($, 'post');

  doSomething();

  sinon.assert.calledOnce(stub);
});

上面的代码中,注意it()的第二个参数,它被sinon.test()包裹。此外注意我们使用this.stub()代替了sinon.stub()

使用sinon.test()包裹测试体可以让我们使用sinon沙盒特性,其允许我们使用this.spy()this.stub()this.mock()来创建spies, stubs和mocks。任何你在沙盒中创建的test-doubles都会自动被清理。

我们上面的代码中并没有stub.restore() – 托沙盒的福它已经不再需要了。

请尽可能使用sinon.test(),你会避免由于前面的测试项没有清理test-doubles而导致的灵异问题。

Sinon并不是黑魔法

Sinon很强大,而且某些时候很难理解它是如何工作的。让我们看一下Sion工作原理的原生javascript的例子,这样我们可以更好的理解其思想。

我们可以自己实现spies, stubs和mocks。使用Sinon只是因为它更方便 – 自己实现会非常复杂。

首先,spy本质上是一个函数wrapper:


//A simple spy helper
function createSpy(targetFunc) {
  var spy = function() {
    spy.args = arguments;
    spy.returnValue = targetFunc.apply(this, arguments);
    return spy.returnValue;
  };

  return spy;
}

//Let's spy on a simple function:
function sum(a, b) { return a + b; }

var spiedSum = createSpy(sum);

spiedSum(10, 5);

console.log(spiedSum.args); //Output: [10, 5]
console.log(spiedSum.returnValue); //Output: 15

我们可以很容易的使用自定义函数来实现spy的功能。但注意sinon的spies提供了非常多的特性 – 包括断言的支持。这让sinon更方便使用。

关于Stub Then?

实现一个简单的stub, 你可以简单的替换成一个新的:


var stub = function() { };

var original = thing.otherFunction;
thing.otherFunction = stub;

//Now any calls to thing.otherFunction will call our stub instead

但是,sinon的stub提供了许多更好用的功能:

  • 它们拥有spy的全特性
  • 你可以调用stub.restore()来恢复原始的行为
  • 你可以结合sinon的断言

Mocks simply combine the behavior of spies and stubs, making it possible to use their features in different ways.

尽管有时候sinon看起来像个“黑魔法”,但它的大多数功能其实很容易自己实现。但比起自己来实现一套来说,sinon非常方便使用。

总结

真实项目的测试有时非常的复杂,导致你可能彻底放弃。但是使用sinon,测试变得非常简单。

记住一个重要的准则:如果一个函数很难被测试,尝试使用test-doubles替换它。

想知道更多关于如何让你的代码使用sinon?当我的网站来,我会提供Sinon in the real-world guide给你,包含了sinon的最佳实践,和三个真实的例子来讲解如何在不同的测试方案中使用它。



原文发布时间为:2018年06月27日
原文作者:玉
本文来源:  掘金  如需转载请联系原作者
相关文章
|
2月前
|
JavaScript 前端开发 网络协议
​Node.js 教程(一) 基本概念与基本使用
​Node.js 教程(一) 基本概念与基本使用
|
1月前
|
Web App开发 Java 测试技术
《手把手教你》系列基础篇之(四)-java+ selenium自动化测试- 启动三大浏览器(下)基于Maven(详细教程)
【2月更文挑战第13天】《手把手教你》系列基础篇之(四)-java+ selenium自动化测试- 启动三大浏览器(下)基于Maven(详细教程) 上一篇文章,宏哥已经在搭建的java项目环境中实践了,今天就在基于maven项目的环境中给小伙伴们 或者童鞋们演示一下。
66 1
|
1月前
|
Web App开发 Java 测试技术
《手把手教你》系列基础篇之(三)-java+ selenium自动化测试- 启动三大浏览器(上)(详细教程)
【2月更文挑战第12天】《手把手教你》系列基础篇之(三)-java+ selenium自动化测试- 启动三大浏览器(上)(详细教程) 前边宏哥已经将环境搭建好了,今天就在Java项目搭建环境中简单地实践一下: 启动三大浏览器。按市场份额来说,全球前三大浏览器是:IE.Firefox.Chrome。因此宏哥这里主要介绍一下如何启动这三大浏览器即可,其他浏览器类似的方法,照猫画虎就可以了。
42 1
|
22天前
|
Web App开发 前端开发 Java
《手把手教你》系列技巧篇(九)-java+ selenium自动化测试-元素定位大法之By name(详细教程)
【4月更文挑战第1天】 这篇教程介绍了如何使用Selenium Webdriver通过name属性来定位网页元素,作为系列教程的一部分,之前讲解了id定位,后续还会有其他六种定位方法。文中以百度搜索为例,详细说明了定位搜索框(name="wd")并输入关键词“北京宏哥”的步骤,包括手动操作流程、编写自动化脚本以及代码实现。此外,还提供了查看和理解Selenium源码的方法,强调了`open implementation`选项用于查看方法的具体实现。整个过程旨在帮助读者学习Selenium的元素定位,并实践自动化测试。
41 0
|
1月前
|
Web App开发 存储 JavaScript
《手把手教你》系列技巧篇(八)-java+ selenium自动化测试-元素定位大法之By id(详细教程)
【2月更文挑战第17天】本文介绍了Web自动化测试的核心——元素定位。文章首先强调了定位元素的重要性,指出找不到元素则无法进行后续操作。Selenium提供八种定位方法,包括By id、name、class name等。其中,By id是最简单快捷的方式。文章还阐述了自动化测试的步骤:定位元素、操作元素、验证结果和记录测试结果。此外,讨论了如何选择定位方法,推荐优先使用简单稳定的方式,如id,其次考虑其他方法。最后,作者提供了Chrome浏览器的开发者工具作为定位元素的工具,并给出了通过id定位的代码示例。
51 0
|
17天前
|
前端开发 Java 测试技术
《手把手教你》系列技巧篇(十二)-java+ selenium自动化测试-元素定位大法之By link text(详细教程)
【4月更文挑战第4天】本文介绍了link text在自动化测试中的应用。Link text是指网页中链接的文字描述,点击可跳转至其他页面。文章列举了8种常用的定位方法,其中着重讲解了link text定位,并通过实例展示了如何使用Java代码实现点击百度首页的“奥运奖牌榜 最新排名”链接,进入相应页面。如果link text不准确,则无法定位到元素,这说明linkText是精准匹配,而非模糊匹配。文章还提到了partial link text作为link text的模糊匹配版本,将在后续内容中介绍。
36 4
|
16天前
|
XML 前端开发 Java
《手把手教你》系列技巧篇(十四)-java+ selenium自动化测试-元素定位大法之By xpath上卷(详细教程)
【4月更文挑战第6天】按宏哥计划,本文继续介绍WebDriver关于元素定位大法,这篇介绍定位倒数二个方法:By xpath。xpath 的定位方法, 非常强大。使用这种方法几乎可以定位到页面上的任意元素。xpath 是XML Path的简称, 由于HTML文档本身就是一个标准的XML页面,所以我们可以使用Xpath 的用法来定位页面元素。XPath 是XML 和Path的缩写,主要用于xml文档中选择文档中节点。基于XML树状文档结构,XPath语言可以用在整棵树中寻找指定的节点。
43 0
|
1月前
|
Web App开发 安全 Java
《手把手教你》系列技巧篇(七)-java+ selenium自动化测试-宏哥带你全方位吊打Chrome启动过程(详细教程)
【2月更文挑战第16天】本文介绍了如何通过查看源码理解Selenium启动Chrome浏览器的过程。首先,展示了启动Chrome的Java代码,包括设置系统属性、创建WebDriver实例、最大化窗口、设置隐性等待、打开网站、获取页面标题以及关闭浏览器。文章还讲解了包(package)、import导入、setProperty设置系统属性、WebDriver接口、driver实例、manage方法、get方法加载网页以及quit方法退出浏览器的基本概念和作用。适合没有Java基础的读者了解Selenium与Java的交互方式。
47 3
|
5天前
|
前端开发 JavaScript Java
《手把手教你》系列技巧篇(二十五)-java+ selenium自动化测试-FluentWait(详细教程)
【4月更文挑战第17天】其实今天介绍也讲解的也是一种等待的方法,有些童鞋或者小伙伴们会问宏哥,这也是一种等待方法,为什么不在上一篇文章中竹筒倒豆子一股脑的全部说完,反而又在这里单独写了一篇。那是因为这个比较重要,所以宏哥专门为她量身定制了一篇。FluentWait是Selenium中功能强大的一种等待方式,翻译成中文是流畅等待的意思。在介绍FluentWait之前,我们来讨论下为什么需要设置等待,我们前面介绍了隐式等待和显式等待。
28 3
|
7天前
|
Java 测试技术 定位技术
《手把手教你》系列技巧篇(二十三)-java+ selenium自动化测试-webdriver处理浏览器多窗口切换下卷(详细教程)
【4月更文挑战第15天】本文介绍了如何使用Selenium进行浏览器窗口切换以操作不同页面元素。首先,获取浏览器窗口句柄有两种方法:获取所有窗口句柄的集合和获取当前窗口句柄。然后,通过`switchTo().window()`方法切换到目标窗口句柄。在项目实战部分,给出了一个示例,展示了在百度首页、新闻页面和地图页面之间切换并输入文字的操作。最后,文章还探讨了在某些情况下可能出现的问题,并提供了一个简单的本地HTML页面示例来演示窗口切换的正确操作。
29 0