4.7 Jasmine插件
Jasmine作为一款流行的JavaScript单元测试框架,拥有大量可以扩展Jasmine功能的插件。本书介绍jasmine-ajax和jasmine-jquery这两个插件。
4.7.1 jasmine-ajax
JavaScript应用经常使用Ajax将数据发送给远程服务器并接收服务器的响应结果。单元测试需要隔离实际的服务器,并且控制Ajax调用的返回结果。为此Jasmine提供插件jasmine-ajax来截获Ajax请求并模拟返回结果。
1.安装jasmine-ajax库
使用npm命令安装jasmine-ajax库。
C:\jasmine-demo>npm install --save-dev jasmine-ajax
安装完毕后目录结构如下:
-- jasmine-demo +-- node_modules | +-- jasmine-ajax | +-- jasmine-core | +-- jquery +-- src +-- spec
2.引用jasmine-ajax库
jasmine-ajax库文件是mock-ajax.js。为了使用jasmine-ajax插件,需要在测试执行页面jasmine-demo\spec\Plugin\SpecRunner.html里引用mock-ajax.js。其代码如下:
<script src="../../node_modules/jasmine-ajax/lib/mock-ajax.js"></script>
3.初始化和卸载jasmine-ajax
如果要测试Ajax调用,需要预先调用jasmine.Ajax.install进行初始化(通常在beforeEach里),替换XMLHttpRequest对象。XMLHttpRequest是现代浏览器内建对象,Ajax调用就是通过XMLHttpRequest对象和后端服务器进行数据交换。XMLHttpRequest对象被替换后,当前页面的Ajax调用都会被jasmine-ajax所截获。
测试完毕后需要调用jasmine.Ajax.uninstall以卸载jasmine-ajax(通常在afterEach中),恢复原有的XMLHttpRequest对象。
describe('jasmine-ajax', function () { beforeEach(function () { jasmine.Ajax.install(); }); afterEach(function () { jasmine.Ajax.uninstall(); }); });
4.模拟Ajax返回结果
jasmine-ajax截获Ajax请求后,即可用于在任意时间调用respondWith函数返回的结果。示例代码如下:
it('should specify response when you need it', function () {
var doneFn = jasmine.createSpy('success');
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (args) {
if (this.readyState == this.DONE) {
doneFn(this.responseText);
}
};
xhr.open('GET', '/site/test/url');
xhr.send();
var request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('/site/test/url');
expect(doneFn).not.toHaveBeenCalled();
request.respondWith({
'status': 200,
'contentType': 'text/plain',
'responseText': 'awesome response'
});
expect(doneFn).toHaveBeenCalledWith('awesome response');
});
以上示例先创建一个spy函数doneFn作为Ajax调用结束的回调函数,然后测试用例发出一个Ajax请求。当然这个请求会被jasmine-ajax截获,使用jasmine.Ajax.requests. mostRecent获取这个请求对象。示例代码如下:
var request = jasmine.Ajax.requests.mostRecent();
jasmine.Ajax.requests会返回RequestTracker对象。以上示例使用了mostRecent成员函数,其他成员可以参考mock-ajax.js里的实现完成:
function RequestTracker() { var requests = []; this.track = function(request) { requests.push(request); }; this.first = function() { return requests[0]; }; …
从这个请求对象的属性如url、method、data()等可以获得Ajax请求的相关信息。此时spy函数doneFn还没有被调用,因为此时还没有设置返回结果。
调用请求对象的respondWith函数可以设置返回结果。本示例设置了状态代码(status)、内容类型(contentType)以及返回文本(responseText)。最后验证spy函数doneFn已被调用。
除了利用responseWith函数在需要的时候返回指定内容,也可以预先设置条件。一旦Ajax请求符合这个条件,jasmine-ajax会立即返回预设结果。示例代码如下:
it('should allow responses to be setup ahead of time', function () { var doneFn = jasmine.createSpy('success'); jasmine.Ajax.stubRequest('/another/url').andReturn({ 'responseText': 'immediate response' }); var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function (args) { if (this.readyState == this.DONE) { doneFn(this.responseText); } }; xhr.open('GET', '/another/url'); xhr.send(); expect(doneFn).toHaveBeenCalledWith('immediate response'); });
上面的示例中使用了stubRequest函数设置条件,这样一旦有Ajax请求访问’/another/url', jasmine-ajax立即返回指定结果,即如下代码的作用。
jasmine.Ajax.stubRequest('/another/url').andReturn({ 'responseText': 'immediate response' });
4.7.2 jasmine-jquery
除了测试异步代码,前端JavaScript单元测试的另外一个难点是测试DOM操作。其难点表现在以下两个方面:
1)加载并清除测试所需的DOM元素
为了加载测试所需的DOM元素,需要预先在SpecRunner.html文件里准备DOM元素。例如为前面示例测试Engine.start函数准备了需要淡出的元素:
<body> <div id="fade-div">some content</div> </body>
或者在测试用例里动态添加以下代码:
$('body').append('<div id="fade-div">some content</div>');
但是这样写代码非常烦琐,而且当测试用例执行完毕后,必须手工清除这些DOM元素,恢复测试环境,以免影响其他测试用例的运行。
2)验证DOM元素的变化
执行被测代码后需要验证DOM元素的变化。示例代码如下:
expect(el.css("display")).toBe("none");
但是Jasmine内建的断言库并没有提供对DOM的特殊支持。针对这个问题,jasmine-jquery插件提供了API来帮助加载并自动清除HTML、CSS和JSON对象,同时它提供非常强大的自定义匹配器,简化对DOM条件的断言。本书主要介绍HTML对象的加载。如果读者对CSS等技术感兴趣,可以参考此网址https://github.com/velesin/jasmine-jquery的相关内容。
1.安装jasmine-jquery库
使用npm命令安装jasmine-jquery库。
C:\jasmine-demo>npm install --save-dev jasmine-jquery
安装完毕后目录结构如下:
-- jasmine-demo +-- node_modules | +-- jasmine-ajax | +-- jasmine-core | +-- jasmine-jquery | +-- jquery +-- src +-- spec
2.引用jasmine-jquery库
jasmine-jquery的库文件是jasmine-jquery.js。在测试执行页面jasmine-demo\spec\Plugin\SpecRunner.html里引用jasmine-jquery.js的代码如下:
<script src="../../node_modules/jasmine-jquery/lib/jasmine-jquery.js"></script>
3.加载HTML DOM元素
在jasmine-demo\src\Plugin\domfunctions.js中准备一个JavaScript函数用来改变DOM元素里的内容,作为被测代码:
/* domfunctions.js */ function changeContainerText(txt) { if (typeof txt === 'undefined') { txt = 'hello world'; } $("#container").text(txt); }
所以在测试执行页面jasmine-demo\spec\Plugin\SpecRunner.html里引用这个js文件:
<script src="../../src/Plugin/domfunctions.js"></script>
测试这个函数需要准备一个id是container的div。jasmine-jquery提供了多种方式加载DOM元素,如表4-6所示。
表4-6 jasmine-jquery HTML DOM加载函数
表4-6提到的“容器”指的是执行单元测试时jasmine-jquery会在测试执行页面SpecRunner.html里动态创建id为jasmine-fi xtures的div元素:
<div id="jasmine-fixtures"> </div>
jasmine-jquery加载的DOM元素会被插入到这个div元素里。
这个“容器”在测试用例结束后会被自动清除,不会影响下一个测试用例的运行。
表4-6里的全局函数都是jasmine.getFixtures()返回对象里的成员函数的简化形式:
loadFixtures()⇒jasmine.getFixtures().load() appendLoadFixtures()⇒jasmine.getFixtures().appendLoad() readFixtures()⇒jasmine.getFixtures().read() setFixtures()⇒jasmine.getFixtures().set() appendSetFixtures()⇒jasmine.getFixtures().appendSet()
使用setFixtures函数直接加载HTML代码片段:
describe('setFixtures', function () { beforeEach(function () { setFixtures('<div id="container"></div><button id="btn" onclick="changeContainer Text()">Click</button>'); }); });
如果HTML片段比较长的话,可以将HTML片段保存到文件中(例如将以上setFixtures函数里的片段保存到jasmine-demo\spec\Fixtures\PluginFixture.html文件),调用loadFixtures函数进行加载:
describe('loadFixtures', function () { beforeEach(function () { var path = ''; if (typeof window.__karma__ ! == 'undefined') { path += 'base'; } jasmine.getFixtures().fixturesPath = path + '/spec/Fixtures'; loadFixtures('PluginFixture.html'); }); });
jasmine-jquery默认会从spec/javascripts/fixtures目录加载HTML文件。这个默认路径可以被修改,例如:
jasmine.getFixtures().fixturesPath = 'my/new/path';
注意:如果在单元测试里使用Karma的话,测试文件会从base/目录里被访问,所以修改jasmine-jquery默认路径时需要加上base/前缀:
jasmine.getFixtures().fixturesPath = 'base/my/new/path';
loadFixture函数会使用Ajax调用来加载HTML文件,因为浏览器默认不允许Ajax加载本地文件。如果在本地直接打开SpecRunner.html文件,以上的示例将无法运行,所以需要使用一个HTTP服务器,并且将HTTP服务器的根目录设为jasmine-demo。这样通过HTTP服务器访问SpecRunner.html,即可使loadFixture成功加载/Spec/Fixtures/PluginFixture.html。
使用npm命令可以全局安装一个简易的HTTP服务器:
npm install http-server -g
然后在命令控制台将当前目录切换到jasmine-demo,运行以下命令:
http-server
这个简易HTTP服务器默认使用8080端口,所以需访问http://localhost:8080/spec/plugin/specrunner.html进行单元测试。
jasmine-ajax会截获所有的Ajax请求,而jasmine-jquery的loadFixture会调用Ajax加载HTML文件,所以当两者一起使用的时候,必须先调用loadFixture,然后再初始化jasmine-ajax。例如:
beforeEach(function(){ // first load your fixtures loadFixtures('fixture.html'); // then install the mock jasmine.Ajax.install(); });
4.验证DOM元素的变化
jasmine-jquery为jQuery框架提供了大量自定义匹配器,如表4-7所示。为了测试以上示例里changeContainerText函数是否更改DOM成功,可以使用以下的测试用例:
表4-7 常用jasmine-jquery自定义匹配器
it('container should have hello world text', function () { expect($('#btn')).toExist(); $('#btn').trigger('click'); expect($('#container')).toHaveText('hello world'); });
读者可以访问https://github.com/velesin/jasmine-jquery了解更多的jasmine-jquery匹配器。