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了。


结论


测试是很有趣的——特别是当我们有这么多的工具可供使用的时候。不管我们有着哪种类型的应用,我们应该明确的是有一种测试它的方法。我希望这篇文章能够帮助你找到合适你的项目的正确工具。

0 0
原创粉丝点击