为了保证程序的质量,一定要做覆盖所有功能的测试。但不应该等到程序做好之后才去搭建测试框架。最好是用单元测试确立一个测试驱动开发(TDD)的模式。 单元测试将代码分解为单个任务(单元),确保每个单元能按设计好的逻辑工作。其核心思想是一旦设置好单元测试,就可以在各种不同的环境和浏览器中运行它,以确保程序能够按照预期的方式运转。这是剔除边界情况的最好办法,否则有可能等到产品发布之后问题才会暴露出来。 是的,做单元测试要比只写功能代码工作量大。但如果能确立TDD的开发方式,你在做Q&A,以及调试只在极罕见情况下才会出现的繁琐问题时,可以节省很多时间。更重要的是,完备的测试集能提升你对交付物的信心,并确保在开发过程中不会漏掉回归的问题。 本节将会介绍如何用Grunt给项目创建一个构建过程。Grunt可以自动合并JavaScript并做最小化处理,可以通过linting检查这些文件的语法,并强制它们遵守代码规范。还会介绍如何用QUnit设置单元测试。多亏了Grunt,只要程序中有文件修改发生,它就会运行单元测试,在编写的同时就能保证代码的质量。 1.4.1 使用Grunt Grunt是一个执行任务的工具,其中一些可以用来构建程序的辅助工具。在把脚本放到生产环境中之前,要做很多重复性的任务,Grunt把它们都自动化了。本节将会讲解,如何用Grunt自动合并JavaScript文件,并对其做最小化处理,然后运行单元测试和linting。你既可以手动运行这些任务,也可以让它们在文件被修改时自动运行。 要用Grunt,必须先按Node网站(http://nodejs.org/)上的指导安装Node.js。Grunt是个命令行工具,所以请打开终端窗口,输入下面的命令,将Grunt的命令行工具安装到全局环境中。 npm install -g grunt-cli 命令行工具装好后,进入项目目录中安装Grunt。 npm install grunt 如果要用Grunt命令行工具,每个项目都要安装Grunt。或者也可以全局安装,然后把它加到你的bash档案中。 然后在项目中创建3个目录: dist src test 这些目录中会放置程序的发布文件和源码文件,以及单元测试文件。接下来要为Grunt创建两个配置文件:一个package.json文件和一个Gruntfile.js文件(两个文件都要放在项目的根目录下)。 1. 构建package.json并安装Grunt插件 package.json中保存了程序的一些基本信息,如下所示。 { "name": "my-project-name", "version": "0.1.0", "devDependencies": {} } 这段JSON代码中只包含了程序的名称和当前版本号(这是你需要填写的信息),然后定义了一些开发时的依赖项。这些依赖项是用在任务运行器中的Grunt插件。但你没必要手动输入它们,NPM安装这些插件时会自动在package.json中加入这个列表。 npm install grunt --save-dev 用--save-dev选项会帮你在package.json文件中自动输入这些依赖项和它们的版本号。接下来装上本例中所需的其他插件。 npm install grunt-contrib-concat --save-dev npm install grunt-contrib-uglify --save-dev npm install grunt-contrib-qunit --save-dev npm install grunt-contrib-jshint --save-dev npm install grunt-contrib-watch --save-dev 2. 构建Gruntfile并创建任务 现在必须创建Gruntfile.js文件,这是Grunt实现的精髓。这个文件中包含了所有配置和你要执行的任务,比如: module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), // 合并 concat: { options: { separator: ';' }, dist: { src: ['src/**/*.js'], dest: 'dist/<%= pkg.name %>.js' } }, // 缩小 uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */n' }, dist: { files: { 'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } } }, // 单元测试 qunit: { files: ['test/**/*.html'] }, // linting jshint: { files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'], options: { // 覆盖JSHint的默认选项 globals: { jQuery: true, console: true, module: true, document: true } } }, // 自动化任务运行 watch: { files: ['<%= jshint.files %>'], tasks: ['jshint', 'qunit'] } }); // 依赖项 grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); // 任务 grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); }; 要弄明白Gruntfile文件中的这些代码,我们要从头开始重新构建它,先从下面这段代码开始: module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json') }); }; 这段代码启动了配置的初始化函数,把package.json中的设置放到了缓存中。那样就可以在Gruntfile中的其他位置引用这些值了,你马上就能见证这一点。接下来配置一个合并程序中脚本的任务。 module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { options: { separator: ';' }, dist: { src: ['src/**/*.js'], dest: 'dist/<%= pkg.name %>.js' } } }); }; 这段代码让Grunt从src/目录中拉出所有的.js文件,把它们合并后用你在package.json中定义的名称保存到dist/目录下。接下来再配置一个任务,用UglifyJS将文件最小化。 uglify: { options: { // banner文本会被添加到输出的顶部 banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */n' }, dist: { files: { 'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } } } 这段代码让UglifyJS将contact任务(contact.dist.dest)中产生的所有文件最小化,然后把它们保存到dist/目录中。接下来配置QUnit。 qunit: { files: ['test/**/*.html'] } 对QUnit的配置只是定义了测试运行文件的位置,后面还会解释如何设置。现在我们要设置一个任务来做代码检查。 jshint: { files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'], options: { // 覆盖JSHint的默认选项 globals: { jQuery: true, console: true, module: true, document: true } } } 这段代码中先设置要用JSHint进行检查的文件,又定义了几个参数。代码检查在任何测试集中都很有价值,因为它会检查JavaScript的语法,找出所有的错误和格式不规范的地方。接下来配置监测插件,它会在文件更改时自动运行任务。 watch: { files: ['<%= jshint.files %>'], tasks: ['jshint', 'qunit'] } 这段配置让监测插件监测程序文件的所有变化,一旦发现就会触发jshint和qunit任务。做好监测配置后,只要你修改了代码,就会自动触发代码检查和单元测试任务。这样你就可以确信代码不会出错,而且还遵守了编码规范。 然后加载之前安装的Grunt插件。 grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); 最后设置好你要运行的任务,最重要的是默认任务。 grunt.registerTask('test', ['jshint', 'qunit']); grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); 要运行默认任务,只要在命令行中输入grunt就可以了。 同样,可以用grunt test运行测试任务。 1.4.2 使用QUnit Grunt设置好了,并且已经用JSHint做了一些测试。但代码检查只会检查JavaScript的语法错误和格式错误,你仍然需要设置单元测试以验证程序的实际运行逻辑。单元测试要确保程序在某些情况下的表现能够符合预期。它们对程序的功能做了精细的分析,对程序执行每一步所得出的结果进行验证,判断其是否符合预期。 在前面配置Grunt时你已经接触过QUnit了,当时是在文件发生变化时运行QUnit测试。但现在test/目录中还没有单元测试。本节会教你如何使用QUnit,以及如何为程序创建测试。QUnit是一个JavaScript测试框架,jQuery和很多其他项目都在用它。它易于使用,并且具备特性丰富的单元测试能力。 1. QUnit基础 先从http://qunitjs.com/上下载脚本和样式表。然后按照首页上的指南为单元测试页面设置好标记,接着运行测试例子。 test('hello test', function() { ok(1 == '1', 'Passed!'); }); test()函数定义了被测试单元的标题(hello test),然后传入了一个测试要运行的匿名函数。在这个例子中,测试ok()中的第一个参数通过了(1=='1'),所以QUnit输出了Passed!,如图1-12所示。 图1-12 该单元测试已通过。QUnit会告诉你它运行得有多快,你还可以再次运行该测试 如果把那个参数换成1=='2',这个单元测试就会失败,如图1-13所示。 图1-13 在单元测试失败后,QUnit会输出一个失败消息及错误来源 另外,即便代码中出现了语法错误,QUnit也会输出一个全局错误,如图1-14所示。 图1-14 一个语法错误抛出了这个全局错误 2. 深入挖掘QUnit 这些都是相当简单的例子。如果想了解QUnit在实际项目中表现如何,可以看一下QUnit在jQuery中的应用,下载地址是https://github.com/jquery/jquery/tree/master/test/unit。 注意,单元测试代码在一个单独的目录中:test/unit/。单元测试决不应该出现在生产环境下的代码库中,它们只能用于调试,并且通常都会包含数量可观的额外代码。 现在请打开core.js并找到test("trim",function {...}),在我写这部分内容时,它是jQuery v1.9.1中的第233行。下面是对jQuery.trim()方法的测试。 test("trim", function() { expect(13); var nbsp = String.fromCharCode(160); equal( jQuery.trim("hello "), "hello", "trailing space" ); equal( jQuery.trim(" hello"), "hello", "leading space" ); equal( jQuery.trim(" hello "), "hello", "space on both sides" ); equal( jQuery.trim(" " + nbsp + "hello " + nbsp + " "), "hello", " " ); equal( jQuery.trim(), "", "Nothing in." ); equal( jQuery.trim( undefined ), "", "Undefined" ); equal( jQuery.trim( null ), "", "Null" ); equal( jQuery.trim( 5 ), "5", "Number" ); equal( jQuery.trim( false ), "false", "Boolean" ); equal( jQuery.trim(" "), "", "space should be trimmed" ); equal( jQuery.trim("ipadxA0"), "ipad", "nbsp should be trimmed" ); equal( jQuery.trim("uFEFF"), "", "zwsp should be trimmed" ); equal( jQuery.trim("uFEFF xA0! | uFEFF"), "! |", "leading/trailing should be trimmed" ); }); 这些断言用一系列可能的输入参数测试jQuery.trim()的功能。这些测试确保传入这些参数时能得到预期的返回值:尾部空格、头部空格、空字符串、布尔值,还有其他任何你能想到的唯一性情况。如你所见,jQuery团队在这些测试中覆盖了范围非常广泛的边界情况。如图1-15所示,jQuery通过了所有这些测试。 图1-15 jQuery.trim()通过了它的所有单元测试 从更深层面上讲,测试函数的第一行是expect(13),这就意味着测试恰好要运行13个断言。接下来是第一个真正的断言,请看: equal( jQuery.trim("hello "), "hello", "trailing space" ); 这一行用到了QUnit的equal()方法,这个方法可以确定前两个参数是否相等。在上面的代码中,这个测试期望jQuery.trim("hello ")返回"hello",在这里它通过了。 用QUnit可以做很多事情,我希望你能看看QUnit的文档(http://api.qunitjs.com),翻翻jQuery的测试,找找如何使用测试框架的感觉。或者签出QUnit本身的测试看看。 3. 设置你自己的QUnit测试 现在你已经掌握了使用QUnit的基础知识,为设置自己的测试做好了准备。这比写不经测试的代码所花的时间要多一些,但如果你是按照最佳实践(面向对象、关注点分离,等等)做的,应该不会觉得太难。 你应该关注的重点是让测试原子化,它们所测试的功能应该尽可能小。那不是说每行代码都要分开来测试,而是说把代码分割成尽可能小的任务,然后每个都能测试到。测试做得越细致,就越容易追踪到测试失败的原因。 你还应该确保测试是完全独立的,测试运行的顺序应该无关紧要,并且每个测试都应该能够单独运行,不需要其他测试参与。另外,要提供测试的文档,确保错误提示消息是有意义的。如果某个单元测试失败了,通常你最不想做的就是再追踪它在检查什么。 最后要申明一点,不要等到程序都做好了才去写测试,应该边做边写。每次添加一个新功能,都应该辅之以一个测试集。要让你自己形成TDD的思维模式,一段代码只有在你完成测试之后才算是真正完成了。 把你写的所有测试都保存在tests/目录下,这样Grunt就能自动运行它们了。现在无论你什么时候对脚本做了修改,Grunt都会执行一次全面的健全性检查,绝不会让有问题的代码溜进你的代码库中。
JavaScript编程实战——1.4 测试
书名: JavaScript编程实战
作者: [美] Jon Raasch
出版社: 人民邮电出版社
原作名: JavaScript programming: pushing the limits
译者: 吴海星
出版年: 2014-3
页数: 320
定价: 59.00
装帧: 平装
ISBN: 9787115345486