最近在慢慢深入Backbone,也试着写一些测试,找一些合适的文档来学习。于是就找到了一个系列的文章 : Testing Backbone applications with Jasmine and Sinon – Part 1
概览
这是第一次展示如何测试Backbone.js应用的一系列文章,在这里我们使用Jasmine BDD测试框架以及Sinon.JS库的spying,stubbingt和mocking。
在这一部分,我们将简单地说一下Backbone,紧接着我们将介绍Jasmine和Sinon的一些细节。在这个过程中,我们将会看到这些工具是如何完美地测试Backbone应用。
无处不在的Backbone
最近的几个月里,随着大量教程的推出以及一些高性能的实现,使得Backbone获得了更多的关注。
我们可以用下面的原因来解释Backbone的流行。它提供了一个最小化的model-view-controller(模式-视图-控制器)的框架来帮助开发者控制复杂的代码,但是与此同时它可以与其他一些框架一起工作。这就意味着,与Cappuccino这些富JS UI框架相比,Backbone不提供UI框架或者主题,但是它可以让开发者去选择DOM库。Backbone对于jQuery或者Zepto支持得特别好,但是你也可以使用别的库来代替他们。
Backbone MVC框架本身可以很好地支持自下而上的单元测试。models,collections,views和routers的隔离,意味着每个部分的行为可以单独地测试,使调试更简单。
对于那些有MVC框架(如Rails和Django)开发测试经验的人来说,这部分的开发也会很熟悉。很有多成熟的单元测试库,工具和方法可以测试这些应用。你只需要去写Javascript测试去保证你的前端代码和你的服务端代码同样的高效。
Jasmine BDD
(这里就不引用原文对于Jasmine的介绍了,简单地来说就是下面这样子)
- 不依赖于其它任何框架;
- 不需要DOM支持(也就是说可以脱离浏览器!);
- 行为驱动式语法,写起来非常简单;
- 支持对异步调用的测试;
- 可以在多种环境中运行(包括Ruby、NodeJS、Scala、Java、BPM、.net、Perl)
Specs
多说无益,让我们来看看一个简单地用于Backbone Model的Jasmine测试。
it("should expose an attribute", function() {
var episode = new Backbone.Model({
title: "Hollywood - Part 2"
});
expect(episode.get("title"))
.toEqual("Hollywood - Part 2");
});
一个spec只是简单地描述了一下预期的行为,上面的代码会执行该行为,以及一种或多种期望测试行为的代码。
个别spec应短而且只测试行为的一个方面。如果你发现自己写了一些不同的期望或spec变得非常长,那么考虑将它变成其他spec。将你的spec与suites分组,并使用set up和teardown函数可以帮助这一点。
Suites
Specs分组为Suites,这被定义在describe()函数。例如,所有的specs为集模式可以被分成一组,如下所示:
describe("Episode model", function() {
it("should expose an attribute", function() {
...
});
it("should validate on save", function() {
...
});
});
很不错的一点是,Suites也可以嵌套。当你有很多specs的,因为你可以将它们组织成组离散块。我喜欢用一个描述块包与特定语境出发specs。这更好地保留了specs的会话风格。例如:
describe("Episode model", function() {
describe("when creating a new episode", function() {
it("should expose the title attribute", function() {
...
});
it("should have a default parental rating", function() {
...
});
});
});
beforeEach() and afterEach()
作为传统风格的xUnit测试框架,可以选择指定的代码在每次测试后运行。这是一个很不错的功能,以确保一致的条件下对每个试验,并用于设置变量和对象中的规格使用。
下面的示例使用beforeEach()来创建一个用于每个sepc的Model实例。
describe("Episode", function() {
beforeEach(function() {
this.episode = new Backbone.Model({
title: "Hollywood - Part 2"
});
});
it("should expose an attribute", function() {
expect(this.episode.get("title"))
.toEqual("Hollywood - Part 2");
});
it("should validate on save", function() {
...
});
});
你可以提供一个beforeEach()和afterEach()方法在每个嵌套的describe,接着在你的specs,让你拥有专为每个Suites的规格一般或者特殊setup()和teardown()。正如您将在本文的其它部分看,这是非常方便的确实减少重复和控制每个specs的确切条件。
The spec runner
这种结构使得specs对于其他开发者能够直易读和容易理解的。,主要是因为对每个specs的说明以及期望的匹配的格式。
Jasmine还提供了一个简单的spec runner,这简直是与将运行所有你所提供的规范的脚本的HTML页面。下面显示了一个suite的specs与单一spec故障的输出:
我们将推出茉莉花其他一些有用的功能在本文的其它部分,因为我们需要他们,包括创建固定装置,使用jQuery和创建自己的自定义期望的匹配。现在,到Sinon.JS。
我们将在本文的其他部分说明一些Jasmine有用的功能,因为我们需要这些功能,包括creating fixtures,使用jQuery和创建自己的自定义期望的匹配。现在,让我们试试Sinon.JS。
Sinon.JS
Sinon.js提供了fake对象——spies,stubs和mocks,是一个专用于Javascript的个测试辅助工具。利用这些结构在测试JavaScript代码是不是已经真正流行起来,只是刚刚开始。但是,如果你正在开发一个丰富的,复杂的应用程序,如您在使用Backbone,然后fake对象是测试工具集的一个非常有用的部分。
Christian Johansen——Sinon.JS的创建者,解释了为什么你需要fake。在Javascript中,这些原因可以分为
- 性能 真实的DOM操作,依靠定时行为及网络活动减慢了测试
- 隔离 单元测试应把重点放在小的一块功能成为可能,并解耦不可靠的或低依赖
使用fake对象是拥抱TDD和BDD的基本组成部分。他们基本上是让代码能够脱离其依赖进行测试。任何API或模块测试你的代码依赖于可以fake的,你需要为你的测试的方式作出回应。您也可以检查fake的方法,看看测试的过程中,究竟他们是如何被调用。
Sinon.JS允许你提供fake几乎所有的东西。您可以将自己的应用程序的fake,在jQuery的,当XMLHttpRequest API本身特定的行为,或者你甚至可以fake JavaScript的计时方法,对于有时间依赖性,如动画和超时快速的测试代码。
Sinon.JS提供了三种类型的fake对象:spies, stubs and mocks。
Spies
(ps:这部分翻译得很烂)
Spies是跟踪的方式和他们被调用的函数,以及返回想要的值。这是异步的,这在事件驱动的应用非常有用,你可以发送一个spy以记录正在发生的事情,即使这些方法都是匿名或封闭。
spies可以“匿名”,也可以对现有功能的spy。
一个匿名的spy只是用spying功能的空函数,可以记录下它是如何被使用的。像一个真正的speis 被送往敌后与连接到它的胸口的麦克风,而测试方法是不知道。这里是一个spy测试一个简单的Backbone自定义事件绑定的例子:
it("should fire a callback when 'foo' is triggered", function() {
// Create an anonymous spy
var spy = sinon.spy();
// Create a new Backbone 'Episode' model
var episode = new Episode({
title: "Hollywood - Part 2"
});
// Call the anonymous spy method when 'foo' is triggered
episode.bind('foo', spy);
// Trigger the foo event
episode.trigger('foo');
// Expect that the spy was called at least once
expect(spy.called).toBeTruthy();
});
这测试将会通过,当spies被调用一次或多次,不管它是如何调用或什么参数。然而,Sinon提供了一些方法,让你严格的限制调用次数,的确是每次调用看起来都像,spy返回了什么。
Spying行为也可以被连接到一个现有的方法。欢快的,我喜欢叫这些“moles”。这是有用的,以检查某些小部分的功能被调用的代码的另一部分与预期一样。例如,您可能要检查模型的保存方法,确保正确的jQuery $.ajax 调用 :
it("should make the correct server request", function() {
var episode = new Backbone.Model({
title: "Hollywood - Part 2",
url: "/episodes/1"
});
// Spy on jQuery's ajax method
var spy = sinon.spy(jQuery, 'ajax');
// Save the model
episode.save();
// Spy was called
expect(spy).toHaveBeenCalled();
// Check url property of first argument
expect(spy.getCall(0).args[0].url)
.toEqual("/episodes/1");
// Restore jQuery.ajax to normal
jQuery.ajax.restore();
});
Stubs and Mocks
保留原文关于Stubs和Mocks的解释
Stubs and mocks in Sinon implement all the features of spies, but with some added features. Stubs allow you to replace the existing behaviour of a particular method with whatever you like. This is great for emulating exceptions and error scenarios from external dependencies so you can test that your code will respond appropriately. It also allows you to start development when other dependencies are not yet in place.
Mocks provide all this, but instead mock an entire API and set built-in expectations on how they will be utilised. Like spies they track how they have been used, and like stubs they respond in a pre-programmed manner according to the needs of the test. However, unlike a spy, the expectations for their behaviour is pre-programmed, and a single verification step at the end will fail if any of these individual expectations are not met.
简单的来说
Stubs就是返回你想要的结果。
Mocks就是确保方法被调用 。
- Stub是代码的一部分。其目的就是用简单的行为替换复杂的行为,从而允许独立地测试代码的一部分。
- Mock Object是使用来代替与你的代码协作的对象的对象,这样代码可以调用Mock Object的方法,这些方法的调用的结果是由你的测试设置好的。
相关文章: mock stub与tdd
Fake Ajax and fake servers
Sinon不局限于Spying on和stubbing普通函数和方法。它还提供了快捷的伪造Ajax响应方式。这意味着您可以从您的JSON数据源完全隔离测试你的代码,并且不依赖于以运行spec suites运行的Web应用程序。此外,你可以测试你的应用程序响应适当的时候将其从幸福的路径,包括无效的JSON和各种HTTP响应代码偏离。
这里有用于Backbone模型spec的获取方法,它使用一个fake的server来响应Ajax请求的一个简单的例子:
describe("Episode model", function() {
beforeEach(function() {
this.server = sinon.fakeServer.create();
});
afterEach(function() {
this.server.restore();
});
it("should fire the change event", function() {
var callback = sinon.spy();
// Set how the fake server will respond
// This reads: a GET request for /episode/123
// will return a 200 response of type
// application/json with the given JSON response body
this.server.respondWith("GET", "/episode/123",
[200, {"Content-Type": "application/json"},
'{"id":123,"title":"Hollywood - Part 2"}']);
var episode = new Episode({id: 123});
// Bind to the change event on the model
episode.bind('change', callback);
// makes an ajax request to the server
episode.fetch();
// Fake server responds to the request
this.server.respond();
// Expect that the spy was called with the new model
expect(callback.called).toBeTruthy();
expect(callback.getCall(0).args[0].attributes)
.toEqual({
id: 123,
title: "Hollywood - Part 2"
});
});
});
这个spec测试可以通过下面这个简单的Backbone Model
var Episode = Backbone.Model.extend({
url: function() {
return "/episode/" + this.id;
}
});
还有更多的兴农,我们这里不讨论。特别是,fake timer是为测试时间依赖的功能非常有用,例如动画,而不会减慢您的测试。
总结
(ps:总结,就是翻译得一般般)
在Backbone应用的带血的世界里,复杂的异步和相互依存的行为可能会导致任何开发人员伤透脑筋。Backbone可以帮助开发人员构建自己的代码转换成小的,独立的models,collections, views和routers。但是,这是真的只是成功的一半。如果没有经过充分测试的代码将会有更多的未被发现的缺陷,而那些被发现将很难追查。其他团队成员可能会无意中破坏你的代码,或者干脆误解了它的目的。