JavaScript客户端测试之旅
来源:互联网 发布:武林外传武功排名 知乎 编辑:程序博客网 时间:2024/05/22 13:19
进行测试是很重要的。测试能让我们不费劲地扩展和重构我们的代码。许多开发者遵循测试驱动的开发流程。我相信写测试能让软件开发变得更有趣,并且通常会带来更好的代码。良好设计以及经过测试的系统更容易维护。
在过去的几年里,开发者开始把许多的应用逻辑放到浏览器中,我们也开始写越来越多的JavaScript代码。因为这种语言非常流行,开发者们开始创建用于改善JavaScript开发体验的工具。在这篇文章中,我们将谈论一些专门用于测试客户端JavaScript代码的工具。
测试设置
我们来讨论一下能够进行测试的工具类型,它们能让我们构建、分组以及运行我们的测试。
测试框架
框架包含一些函数如suite、describe、test 或 it。这些能让我们创建测试的分组,这些分组经常被称为套件(suit)。例如:
describe('Testing user management',function(){
it('should register a new user',function(done){
// the actual testing goes here
});
it('should remove a user',function(done){
// the actual testing goes here
});
});
我们把应用逻辑分割成块,每个块都有自己的套件。该套件包括我们想在代码上运行的相关测试。流行的JavaScript测试框架有QUnit、Jasmine 或Mocha.
声明库
我们用声明库来做实际的检查。它们提供了易用的函数,如下面的例子:
expect(5).to.be.a('number');
有很多的模块可供我们使用。Node.js甚至就有一个内置的模块,也有一些开源的工具供我们选择,如Chai、Expect 或 should.js。
应该提醒的是,一些测试框架拥有自己的断言库。
运行器
我们可以需要或者也可以不需要一个运行器(runner)。有些情况下单一的测试框架不能满足,因而我们需要在一个特定的上下文来运行测试。为了完成这件事,我们使用一个运行器。有些情况下这些工具被称为“spec runners”或“test runners.”。这些工具包装了我们的测试套件,并且在一个特殊的环境下运行。我们将会讲到这一点。
应用程序
我们需要一个待测试的应用程序。尽管这只是用于举例说明,但它不能太过简单。TODOMVC 看起来是个不错的选择。它基本上跟其他的TODO app一样,使用了许多不同的框架。我们用Backbone.js 的变种。这就是该应用程序的样子:
假设这是我们上个月在做的一个项目,并且计划下周发布。我们想确保它通过一些测试,也假设后端的已经测试过了。唯一的问题是客户端的JavaScript代码。在这个时候,由于应用程序已经完成了,我们最感兴趣的是它是否能正常地工作。它是一个TODO app,因此我们需要保证用户能够添加、删除以及编辑任务。
在浏览器中测试
我们需要对浏览器中运行的代码执行检查,因此在浏览器中进行测试是很合理的。我们将使用Mocha来作为框架。由于该框架不带断言库,我们将在项目中引用Chai。我们从todomvc.com 下载了app,并且浏览了文件:
├──bower_components
├──js
│ ├──collections
│ ├──models
│ ├──routers
│ ├──views
│ └──app.js
└──bower.json
└──index.html
└──readme.md
这篇文章是关于测试的,因此我们不打算深入Backbone.js工作原理的细节。然而,这里介绍一下文件和目录的基本细节:
bower_components – 包含Backbone.js库、本地存储助手、jQuery、Underscore.js以及TODOMVC通用文件
js – 这个目录包含这个app的实际代码
bower.json – 定义项目的依赖
index.html – 包含HTML标签(模板)
在平常的开发流程中,我们打开一个浏览器、加载应用程序并且使用UI。我们在输入框敲入了一些文字新建了一个TODO,按Enter键然后任务显示在下面。通过点击小小的X标志来移除记录,通过双击TODO来编辑任务。我们有很多涉及不同鼠标事件的动作。
为了测试app,我们不想一遍遍重复上面的步骤。自动化测试可以帮到我们。我们将有效地运行这个应用程序,并且写能够与页面交互就像我们手动操作一样的代码。让我们创建一个tests_mocha文件夹存放我们的测试。
├──bower_components
├──js
│ ├──collections
│ ├──models
│ ├──routers
│ ├──views
│ └──app.js
├──tests_mocha
│ ├──package.json
│ ├──spec.js
│ └──TestRunner.html
└──bower.json
└──index.html
└──readme.md
Mocha和Chai都可以通过npm来安装,npm来自Node.js。我们只需把它们添加到package.json文件,并且在同一个目录下运行npm install。
// package.json
{
"name":"project",
"version":"0.0.1",
"devDependencies":{
"chai":"1.10.0",
"mocha":"2.1.0"
}
}
创建测试运行器
spec.js会包含我们的测试,TestRunner.html 是我们将要修改的副本。我们需要让应用程序跑起来,因此肯定会用到index.html的代码。在讨论改变之前先看一看原始文件是什么样的:
<!doctype html>
<htmllang="en"data-framework="backbonejs">
<head>
<metacharset="utf-8">
<title>Backbone.js • TodoMVC</title>
<linkrel="stylesheet"href="bower_components/todomvc-common/base.css">
</head>
<body>
<sectionid="todoapp">
... the base markup of the application
</section>
<footerid="info">
... the markup of the footer
</footer>
<script type="text/template"id="item-template">...</script>
<script type="text/template"id="stats-template">...</script>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/underscore/underscore.js"></script>
<script src="bower_components/backbone/backbone.js"></script>
<script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/views/todo-view.js"></script>
<script src="js/views/app-view.js"></script>
<script src="js/routers/router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
客户端的app会变得相当复杂,我们的一个app需要跑几个程序。文件末尾所有的<script>标签需要包好代表模板的<script>标签。<section> 也很重要,因此也保留它。
为了看测试的结果需要添加Mocha的样式,我们只测试JavaScript。因此TODOMVC的base.css可以移除掉,替换头部中的<link>标签:
<link rel="stylesheet"href="./node_modules/mocha/mocha.css">
样式去掉了但是标记仍然在那里。然而,我们不想让它可视,因此我们添加另外的CSS类来隐藏它:
<styletype="text/css">
#todoapp, #info{
display:none;
}
</style>
有了这两处变化,我们仍然可以让项目运行和工作,但它的界面是不可见的。我们有一个空的页面来等待我们的测试。下面的代码在页面的底部运行,就在TODOMVC的脚本之后:
<divid="mocha"></div>
<scriptsrc="./node_modules/mocha/mocha.js"></script>
<scriptsrc="./node_modules/chai/chai.js"></script>
<script>
mocha.ui('bdd');
mocha.reporter('html');
varexpect = chai.expect;
</script>
<scriptsrc="./spec.js"></script>
<script>
mocha.run();
</script>
空的<div>被模板用来显示结果,在那之后我们添加Mocha和Chai。最后,我们运行测试。注意,我们的spec.js是在测试框架和声明库被初始化时在mocha.run()之前添加的。实际上,浏览器会:
导入Mocha的CSS;
导入TODOMVC的文件并且运行应用程序;
导入Mocha和Chai;
导入我们的spec.js;
运行测试
编写测试
准备就绪,让我们开始写测试。之前我们提到过要检查三件事情:用户能否添加、删除、编辑待完成任务。
// spec.js
describe("Testing TODOMVC",function(){
varsetText = function(text,selector){
varinput = $(selector || '#new-todo');
vare = $.Event("keypress");
e.which = e.keyCode = 13;
returninput.val(text).trigger(e);
};
before(function(){
window.localStorage.removeItem('todos-backbone','');
app.todos.reset();
});
it("Adding new TODOs",function(){
setText('TODO A');
setText('TODO B');
expect($('#todo-list li').length).to.be.equal(2);
});
it("Deleting TODO",function(){
$('#todo-list li:first-child .destroy').click();
expect($('#todo-list li').length).to.be.equal(1);
});
it("Edit and add TODOs",function(){
setText('A new TODO');
$('#todo-list li:first-child').addClass('editing');
setText('A new TODO','#todo-list li:first-child .edit').blur();
expect($('#todo-list li').length).to.be.equal(2);
expect($('#todo-list li label').eq(0).text()).to.be.equal($('#todo-list li label').eq(1).text())
});
});
测试从调用describe 函数开始,把我们的检查放在一个测试套件里。setText 函数是用来改变输入框的值并模拟按下Enter 键。
大多数测试框架允许我们在测试运行之前执行逻辑,这就是我们用before 函数的原因。在本例中,我们需要清空保存在localStorage 中的数据,因为之后我们期待看到列表中待完成任务的具体数目。
接下来的it 调用表示添加、删除、编辑的三种操作。注意我们用的$ (jQuery)和expect 都是全局函数。
运行测试
完成这段代码,就可以用我们最喜欢的浏览器打开TestRunner.html,并且结果显示如下:
我们现在能保证我们的项目提供了所需的功能。我们有一个测试并且它是有点自动化的。之所以说“有点”是因为测试在浏览器中运行,并且我们仍然需要手工打开TestRunner.html。
这就是使用这种的问题之一。我们不能强迫开发者一直在浏览器中运行测试。测试过程应该是配置或提交过程的一部分。为了达到这个目标,我们需要将测试移到终端。
使用PhantomJS进行测试
下面我们要解决的一个问题是,找到一个能在终端运行的浏览器。我们需要一个浏览器,因为Backbone.js需要渲染UI并且我们要跟它交互。有一种被称为没有界面的(headless) 特殊的浏览器,它并没有一个可视化界面。
我们通过代码来控制它。但是,它们跟真实的浏览器功能一模一样。最流行的一款是PhantomJS。尝试一下看它怎样处理我们的测试。
PhantomJS以可执行文件的形式发布。换句话说,当成功安装之后我们有一个命令变量phantomjs 。它适用于几乎每一个操作系统。同该浏览器一同使用,我们将使用Node.js和它的npm。
在终端运行测试
假设我们已经安装好了PhantomJS,下一步是在终端运行我们的Mocha测试。实际过程中,我们需要在浏览器中加载TestRunner.html,检查来自Mocha框架的结果。我们可以手动操作。但是为了节省时间,我们使用Node.js中的mocha-phantomjs 模块。
快速运行npm install -g mocha-phantomjs会让mocha-phantomjs可在控制台中使用。直接将tests_mocha文件复制到新的目录tests_mocha-phantomjs下。我们要做的唯一的变动就是将
<script>
mocha.run();
</script>
变为:
<script>
if(window.mochaPhantomJS){mochaPhantomJS.run();}
else{mocha.run();}
</script>
模块从测试框架获得回应,并将它发送到终端。这就是结果应该有的样子:
现在我们的检查在终端里运行,我们能添加mocha-phantomjs调用到我们的持续整合设置。
PhantomJS很好,但是它在单一的浏览器中运行我们的代码。如果我们在不同的浏览器中测试代码呢?特别是针对客户端的JavaScript,我们应该保证我们的应用程序能在不同的环境中工作。并且不止如此,我们想在真实的浏览器中测试。Karma 项目能提供这个功能。
使用Karma作为测试运行器
与mocha-phantomjs类似,Karma 以Node.js模块的形式发布。但是,这一次我们不仅需要一个模块,还需要其它几个模块。因此,让我们按照上述的想法,将tests_mocha拷贝到新的文件tests_karma里。package.json文件应当是这个样子:
{
"name":"project",
"version":"0.0.1",
"devDependencies":{
"karma":"https://github.com/krasimir/karma/tarball/589f55a8abab613ec915a871ee976ca0e15ad36f",
"karma-mocha":"^0.1.10",
"karma-phantomjs-launcher":"^0.1.4",
"karma-chrome-launcher":"0.1.7",
"karma-chai":"0.1.0"
}
}
除了上面的包,我们还需要karma-cli。在运行npm install(这会安装上面列出的依赖)之后再运行npm install -g karma-cli。 karma-cli 有karma 命令,我们可以调用它来在终端运行测试运行器。
使用KARMA的问题
我在过去几个月看了很多有关Karma的东西,并且真的想尝试它。但是,我发现它被设计专门用于单元测试,而不是集成测试。
运行器的命令行工具接受JSON对象格式的配置,有一些选项,但是没有一个接受HTML文件。我们可以发送JavaScript文件到浏览器,但是我们不能定义要加载的HTML。
有一个Karma使用的上下文模板,它的所有功能是注入JavaScript代码和测试。为了使用方便,这是不够的。我们有HTML标记及<script>中的模板。在运行应用程序之前它们应该已经被加载到页面中了。
解决方案
我所做的是fork这个项目,并且在配置中增加了一个选项。这可以让我们设置上下文模板,这也是我为什么在package.json 文件中添加一个URL。
保持spec.js不变,把TestRunner.html变为:
<!DOCTYPE html>
<html>
<head>
<title></title>
<metahttp-equiv="Content-Type"content="text/html; charset=UTF-8" />
</head>
<body>
<sectionid="todoapp"> ... </section>
<footerid="info"> ... </footer>
<script type="text/template"id="item-template">...</script>
<script type="text/template"id="stats-template">...</script>
<script type="text/javascript">
if(window.opener){
window.opener.karma.setupContext(window);
}else{
window.parent.karma.setupContext(window);
}
%MAPPINGS%
</script>
%SCRIPTS%
<script type="text/javascript">
window.__karma__.loaded();
</script>
</body>
</html>
需要的<section>和 <footer>都包含在模板中了,最后的代码是Karma用于构建最终文档及运行测试。
配置
我提到框架使用了一个配置文件。使用下面的内容创建一个tests_karma/karma.conf.js文件:
// karma.conf.js
module.exports = function(config){
config.set({
basePath:'',
client:{
contextFile:'/TestRunner.html'
},
frameworks:['mocha','chai'],
files:[
'../bower_components/todomvc-common/base.js',
'../bower_components/jquery/dist/jquery.js',
'../bower_components/underscore/underscore.js',
'../bower_components/backbone/backbone.js',
'../bower_components/backbone.localStorage/backbone.localStorage.js',
'../js/models/todo.js',
'../js/collections/todos.js',
'../js/views/todo-view.js',
'../js/views/app-view.js',
'../js/routers/router.js',
'../js/app.js',
'spec.js'
],
exclude:[],
preprocessors:{},
reporters:['progress'],
port:9876,
colors:true,
logLevel:config.LOG_INFO,
autoWatch:false,
browsers:['PhantomJS','Chrome'],
singleRun:false
});
};
在这个配置里,我们列出了要注入到页面的JavaScript文件,contextFile 设置是我之前讨论过的变化。注意,我们有两种浏览器来运行测试。
运行KARMA
最后一步是在终端运行karma start ./karma.conf.js –single-run。结果如下:
在这一部分的开头karma-phantomjs-launcher和karma-chrome-launcher。该框架使用两个模块来运行我们指定的浏览器。
因此,我们尝试在浏览器中运行测试,但是这个方法不能扩展。我们通过mocha-phantomjs让其在终端运行,但是那意味只在一个浏览器中测试。第三种尝试是使用Karma作为运行器,开启两个不同的浏览器,其中一个不是没有界面的(headless)浏览器。最后一个步骤有点复杂,包括一些模块及一个框架补丁。让我们尝试另一个运行器——DalekJS。
使用DalekJS测试
DalekJS使用一种不同的方法,它不需要测试框架或者断言,已经自带了。
安装非常简单。我们再一次需要Node.js和npm ,因为这个工具是以Node.js的包发布的。跟Karma一样,我们也需要命令行客户端,以及DalekJS框架本身。
//afterthislinewewillhave`dalek`commandavailable
npminstalldalek-cli-g
我们创建另一个tests_dalekjs 文件夹,包含package.json 文件:
{
"name":"project",
"version":"0.0.1",
"dependencies":{
"dalekjs":"0.0.9"
}
}
当我们安装好了两个模块,就可以进行测试了。
编写测试
好消息是我们不需要接触HTML,只需把index.html的文件路径拷贝到tests_dalekjs/TestRunner.html文件中,剩下的步骤都一样。
因为DalekJS有自己的语法,我们不能像上面的测试使用spec.js文件。下面是使用DalekJS API所写的三种操作:
varsetText=function(text,selector){
varinput=$(selector||'#new-todo');
vare=$.Event("keypress");
e.which=e.keyCode=13;
input.val(text).trigger(e);
};
module.exports={
'Testing TODOMVC app':function(test){
test
.open('TestRunner.html')
.execute(setText,'TODO A')
.execute(setText,'TODO B')
.assert.numberOfElements('#todo-list li',2,'Should have two TODOs')
.execute(function(){
$('#todo-list li:first-child .destroy').click();
})
.assert.numberOfElements('#todo-list li',1,'Should have two TODOs')
.execute(setText,'A new TODO')
.execute(function(){
$('#todo-list li:first-child').addClass('editing');
$('#todo-list li:first-child .edit').val('A new TODO').blur();
})
.assert.numberOfElements('#todo-list li',2,'Should have two TODOs')
.assert.text('#todo-list li:first-child label').is('A new TODO')
.assert.text('#todo-list li:second-child label').is('A new TODO')
.done();
}
};
再一次,我们有一个帮助的方法来设定输入域的值,并且触发Enter 按钮。我们导出的对象包含TODO的添加、删除、编辑。像这样设计API是很友好的,因为我们仅通过看方法的执行来知道正在发生什么。
有一个比较麻烦的方法是执行。它接受一个在浏览器上下文下执行的函数。
运行测试
我们使用dalek ./tests_dalekjs/spec.js运行测试,结果:
我们应该记住,DalekJS跟Karma一样,能够在Chrome、IE、Firefox 和 Safari下运行。相当多的现代浏览器是支持的,我们所要做的是安装额外的模块,如 dalek-browser-chrome。更多有关支持的浏览器,点这里。
Atomus——测试的另一种工具
Atomus 是我工作时使用的工具,上面所有的功能选项都很棒。它们中的大多数经过了大型社区的良好测试。然而,在我看来,它们并非是理想的。
我们能覆盖用户的全部过程是很好的,但是通常情况下我们只需要测试应用程序的某一个部分。如果我们只想测试Backbone.js应用的特定视图呢?使用DalekJS这是很困难的,使用Karma可行但是有点麻烦。模块mocha_phantomjs跟工作方案很接近但是也有一些局限。
当我们进行单元测试时,我们意识到我们所做的是DOM模拟。在大多数情形下我们对UI不感兴趣,而对它的行为感兴趣。此时我们找到了jsdom,它是WHATWG和HTML标准的Javascript的实现。
我们创建了一些测试,发现它工作得非常好。它支持DOM操作和DOM事件调度/监听。甚至支持Ajax请求。Atomus是jadom的包装器,提供一个强健和友好的API。
创建测试
让我们以使用Atomus进行的TODOMVC测试来结束这篇文章。我们再一次用到Mocha 和Chai。在新目录tests_atomus创建package.json,定义依赖:
{
"name":"project",
"version":"0.0.1",
"devDependencies":{
"chai":"1.10.0",
"mocha":"2.1.0",
"atomus":"0.1.12"
}
}
TestRunner.html跟原始的index.html文件一样——唯一引入的外部JS文件应该移除掉,因为spec,js 中已经引入过了。文件以下面的代码开始:
varfs=require('fs');
varexpect=require('chai').expect;
describe("Testing TODOMVC",function(){
varatomus=require('atomus');
varhtmlStr=fs.readFileSync('./TestRunner.html').toString('utf8');
varbrowser,$;
});
我们使用文件系统API来读取TestRunner.html的内容。browser 变量代表Atomus API。Atomus库会自动注入jQuery,因此$可以作为快捷符号。第一个测试是这样:
it("Adding new TODOs",function(done){
browser=atomus()
.external(__dirname+'/../bower_components/todomvc-common/base.js')
.external(__dirname+'/../bower_components/jquery/dist/jquery.js')
.external(__dirname+'/../bower_components/underscore/underscore.js')
.external(__dirname+'/../bower_components/backbone/backbone.js')
.external(__dirname+'/../bower_components/backbone.localStorage/backbone.localStorage.js')
.external(__dirname+'/../js/models/todo.js')
.external(__dirname+'/../js/collections/todos.js')
.external(__dirname+'/../js/views/todo-view.js')
.external(__dirname+'/../js/views/app-view.js')
.external(__dirname+'/../js/routers/router.js')
.external(__dirname+'/../js/app.js')
.html(htmlStr)
.ready(function(errors,window){
$=window.$;
browser.keypressed($('#new-todo').val('TODO A'),13);
browser.keypressed($('#new-todo').val('TODO B'),13);
expect(window.$('#todo-list li').length).to.be.equal(2);
done();
});
});
我们在这里初始化虚拟浏览器、定义外部的js文件、设定HTML标记的页面。ready函数接收一个回调,但页面和资源全部被加载完成之后会触发。我们接收一个window对象,跟实际浏览器中的window对象一样。我们可以认为变量是指向浏览器的API的,当然也是指向全局作用域的。
Atomus有一个keypressed 和clicked 的方法模拟用户交互。我们也可以使用流行的jQuery方法来获得同样的结果,但是这些内置的方法可能会带来一些bugs。
既然我们有了browser 变量,我们能继续删除和编辑TODO了。
it("Deleting TODO",function(){
$('#todo-list li:first-child .destroy').click();
expect($('#todo-list li').length).to.be.equal(1);
});
it("Edit and add TODOs",function(){
browser.keypressed($('#new-todo').val('A new TODO'),13);
$('#todo-list li:first-child').addClass('editing');
$('#todo-list li:first-child .edit').val('A new TODO').blur();
expect($('#todo-list li').length).to.be.equal(2);
expect($('#todo-list li label').eq(0).text()).to.be.equal($('#todo-list li label').eq(1).text())
});
运行测试
为了运行测试我们不需要命令行客户端,只需要键入mocha ./spec.js ,结果是:
注意,当我们使用Atomus时主要的事情不是运行器或者没有界面的浏览器——它是测试框架。就是它在驱动测试,Atomus只是一个帮助工具。
在我们的案例中,它帮助我们覆盖不同层次的客户端架构测试。我们有简单UI元素的测试,同时使用同样的工具来整合测试。
为了完成一些有趣的事情,我们看下面的代码:
varmockups=[
{
url:'/api/method/action',
method:'GET',
response:{
status:200,
responseText:JSON.stringify({"id":"AAA"})
}
}
];
varatomus=require('../lib');
varb=atomus()
.ready(function(errors,window){
b.addXHRMock(mockups);
var$=window.$;
$.ajax({
url:'/api/method/action'
}).done(function(result){
console.log(result.id);// AAA
});
});
在复杂的环境中,当应用程序的UI跟后端有通信时,我们需要模拟HTTP请求。在我们的案例中,我们想要涵盖一个完整的使用过程,但是这实际上变得很复杂,因为我们想要进行单独的测试。因此,我们模拟传统的XMLHttpRequest 对象,现在开发者可以通过不同的响应链接到URL了。
结论
测试是很有趣的——特别是当我们有这么多的工具可供使用的时候。不管我们有着哪种类型的应用,我们应该明确的是有一种测试它的方法。我希望这篇文章能够帮助你找到合适你的项目的正确工具。
- JavaScript客户端测试之旅
- 客户端javascript之window
- JavaScript之客户端存储
- 软件测试之客户端(Client)测试
- javaScript之typeof测试
- 小项目之并发测试(客户端)
- appium 自动化测试之Android客户端
- HTML5移动客户端开发之CSS3、Javascript
- 客户端javascript之脚本化文档
- JavaScript工具之客户端调试用JS
- JavaScript客户端接口之SuperMap.Handler
- SharePoint之JavaScript客户端对象模型
- Protobuf之JavaScript客户端简单应用
- web前端之JavaScript权威指南(客户端JavaScript)
- 客户端测试
- 客户端测试
- 客户端JavaScript
- 客户端JavaScript
- Flume之Failover和Load balancing原理及实例
- 浅谈AppStore中的评分与评论
- Python应用指定路径下的模块
- Static用法总结
- PEP8 Python 编码规范整理
- JavaScript客户端测试之旅
- Android_06_横竖屏切换
- 互联网的不同圈子
- 排序算法之归并排序
- GTK+图形化应用程序开发学习笔记(二)—Glib库
- JavaScript 之call , apply 和prototype 介绍
- Android Studio(开发工具)
- elasticsearch相关重要配置说明
- SQL Server中截取字符串常用函数