HTML5中国

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1245|回复: 0

[教程] 用 Mocha、Chai 进行 Node.js 测试

[复制链接]

该用户从未签到

发表于 2015-11-23 17:15:15 | 显示全部楼层 |阅读模式
本帖最后由 执剑为何 于 2015-11-23 17:23 编辑

用Mocha,Chai进行Node.js测试
使用测试驱动开发将会大大简化编写代码的思考过程,并使它变得更加简单,更加快捷。但是仅仅编写测试是远远不够的,真正重要的东西是了解编写测试的类型以及如何组织代码来符合测试的模式。在本文中,我们将学着如何使用TDD模式在Node.js中编写一个小型app。
除了我们都很熟悉的简单的单元测试之外,我们还需要对Node.js中的异步代码进行测试,这为我们的测试增加了额外的困难,因为我们并不总是了解函数运行的顺序或者我们想要测试一些在回调函数中的东西,又或者我们想要看看异步函数是否真正的执行了。
在本文中,我们将创建一个Node app,它能够通过一个给定的查询来搜索文件。虽然这样的东西已经很多了,但是为了展示TDD编程模式,我们还是来从头创建这个app。
第一步当然是编写一些测试,但是在编写测试之前,我们需要选择一个测试框架。你可能会选择使用原生Node,因为其中已经包含了一个assert库,但是它并不算是一个真正的测试运行器,同时也太底层了。
另一个很受欢迎的测试框架是Jasmine。它很好的做到了自包含,你无需添加其他的依赖就可以将它添加到你的脚本本,并且语法清晰易读。Jasmine确实是一个好框架,你完全可以使用Jasmine的node斑斑Jasmine-node来使用你属性的Jasmine语法在浏览器中进行测试。但是我们今天并不打算使用这个框架,而是使用另一个简单有趣的测试框架 – Mocha(官网:http://mochajs.org/)。
我们要创建什么?
在本文中,我们将使用Mocha以及Chai断言库(官网:http://chaijs.com/)来进行测试工作。
和Jasmine以及其他完整的测试套件不同,Mocha只关心总体的结构,而对于实际的断言毫不关心。这不仅允许我们持续观察测试,同时也允许我们自由选择使用自己喜欢的断言库。
在本文的例子中,如果你想要使用原生的assert库,你可能需要为它添加一些额外的结构。
Chai是一个非常好的assert库的替代品。即使你不使用任何的插件,原生的API也为你提供了三种不同的语法来让你决定是到底采用传统的TDD风格还是更加啰嗦一点的BDD风格。
既然我们已经了解到了要使用什么东西,我们现在就来进行安装工作。
设置
首先我们要全局安装Mocha:
  1. npm install -g mocha
复制代码
接下来,我们为我们的项目创建一个新的文件夹并在其中运行下面的代码:
  1. npm install chai
复制代码
上面的代码将会在我们的项目中安装一个Chai的本地副本。接着,我们需要在项目目录下创建一个叫做test的文件夹,它用作Mocha默认的测试路径。
设置非常简单,下一步我们要讨论的是如何格局测试驱动开发的步骤来组织项目的结构。
组织你的项目
在采用驱动测试开发是,很重要的一点是了解应该测试什么不应该测试什么。其中,首要原则就是不要为已经测试过的代码编写测试。这句话的意思是:假设你想要用代码打开一个文件,你并不需要去单独去测试fs函数,因为它是Node语言的一部分,已经通过了严格的测试。同样,当你使用一个第三方库的时候,你不应该去测试其中的函数。你永远不应该去测试已经测试过的代码,这有违TDD测试的过程。
当然,对于每一种编程风格,我们都会有许多不同的选择来决定如何进行TDD。其中一个比较好的方法就是,你在你的app中创建一些单独的组件,每一个组件解决一个单独的函数问题。这些组件使用TDD来创建,以确保它们能够像期望中一样运行,你也不会去破坏它们的API。接着你再去编写你的主要脚本,基本上来说都是些起到粘合作用的代码,它们在特定的情况之下是不需要进行测试的。
上面这段话同样也意味着你编写的大多数组件都能在未来被重用,即使它们现在不会直接在主要的脚本中被使用。
因此,普遍的作法是创建一个叫做lib的文件夹用来放置所有的单个组件。到目前为止,你已经安装好了Mocha和Chai,并且在项目文件夹中已经有了两个子文件夹:’lib’和’test’。
开始进行TDD
如果你是一个TDD新手,不要担心,我们会在这里简单的回顾整个TDD的过程。基本的原则就是除非测试运行器告诉你需要写代码,否则不要写代码。
本质上来说,你编写的测试就是在实际被编写出来之前,你的代码应该做些什么事。在编写代码时,你有一个关注的目标,因此你永远不会被另外的想法所困扰。除此之外,因为你的所有代码都经过了测试,你永远不需要但担心这些代码会在未来毁掉你的app。
一段测试,实际上,仅仅是一个函数在运行时期望做什么的声明,接下来你运行你的测试运行器,显而易见它当然会失败(因为你现在根本就没有写代码)然后接下来你编写了尽可能少的代码使得测试通过。注意,永远不要跳过这一步,因为有事及时你没有添加任何代码,测试也会通过,原因可能是其他部分的代码具有同样的作用。当发生这种情况时,你要么编写更多的代码去做不同的测试,要么这压根就是一个糟糕的测试(通常是因为指向不清楚)。
根据上面提到的法则,如果测试马上就通过了,那么你不能写任何代码,因为它告诉你不要这样做。通过持续不断的编写测试然后实现功能,你将会创建你可以依赖的坚实的模块。
一旦你实现并测试完了你了组件,你可以回头去重构并优化它们,然后继续测试确保重构不会造成测试无法通过,另外很重要的一点是,不要添加任何为经测试的功能。
每一个测试框架都有它自己的语法,但是它们通常都遵循着同样的模式,即先做断言,然后检查是否通过。既然我们使用Mocha和Chai,我们首先从Chai的语法来说起。
Mocha和Chai
在这里,我们将使用’Expect’BDD语法,还记得我们在前面提到了Chai自带几种不同的语法选择吗?这种语法的规则是,首先你调用expect函数,为它传递一个你想要做断言的对象,然后你在后面链式添加一个指定的测试。下面是一个简单的例子:
  1. expect(4+5).equal(9);
复制代码
这是最基本的语法,在这个例子中我们期望4和5相加等于9.这并不是一个好测试,因为4和5会在函数被调用之前就被Node.js相加起来,因此我们在这里实际上只是测试了我们的数学技巧,但是通过这个例子我们可以看到Chai语法的一些基本思想。另一个你需要注意的地方是,以上的语法并不易读,如果和英文句子的格式相比的话。基于这一点,Chai添加了下面的链式getter,它们不会做任何事,但是你可以将它们添加进入断言中是的句子变得啰嗦但是增加了易读性。链式getter的列表如下所示:
  • to
  • be
  • been
  • is
  • that
  • and
  • have
  • with
  • at
  • of
  • same
  • a
  • an
使用上面的链式getter,我们重写了前面提到的测试:
  1. expect(4+5).to.equal(9);
复制代码
我们还可以非常轻松的将运算符取反,只需要加上 .not
  1. expect(4+5).to.not.equal(10);
复制代码
即使你从来没用过这个库,你也不会觉得测试语句有多难理解。
下面我们要简要的讲述一些如何在Mocha中组织代码结构。
Mocha
Mocha是一个任务运行器,因此实际上它并不会太关心测试本身,它关心的只是测试的结构,测试的结构决定着怎样让测试知道代码运行失败了以及怎样显示结果。你创建代码的方式,就是使用多个 describe 块来展示你的库中的不同组件,然后在其中添加 it 块来制定一个特别的测试。
下面是一个简单的例子,假设我们现在有一个JSON类,这个类有一个函数来解析JSON,同时我们想要确保这个解析函数能够检测到一个格式有错误的JSON字符串,我们可以如下组织代码:
  1. describe('JSON',function(){
  2.     describe('.parse()',function(){
  3.         it('should detect malformed JSON strings',function(){
  4.             //测试的代码在此
  5.         });
  6.     });
  7. });
复制代码
这段代码并不复杂,其中的80%都是由你自己决定的,但是如果你保持这样的格式,测试结果将会以一种非常清晰易读的方式显示出来。
我们现在已经准备好开始编写我们的第一个库了,我们先从一个非常简单的同步模块开始,目的是让我们更加熟悉这个系统。我们的app将会需要接受命令行参数来进行设置像是需要搜索多少层级的文件以及测试本身。
为了解决上面的问题,我们将要创建一个模块来接受命令行字符串并解析出其中选项包含的值。
Tag模块
这是一个非常好的模块例子,你可以将这个模块运用到你所有的命令行app中,正如我们现在要编写的这个app。npm上有一个叫做CITags的模块,我们将要创建的是这个模块的一个简化版本。首先,我们在lib文件夹中创建一个叫做tags.js的文件,同时在test文件夹中创建一个叫做tagsSpec.js的文件。
我们需要在测试文件中引入Chai的expect函数,因为我们需要在测试中使用expect断言语法,同时我们也需要引入实际的tags文件来测试它。上面的这些初始化步骤如下所示:
  1. var expect = require('chai').expect;
  2. var tags = require('../lib/tags.js');

  3. describe('Tags',function(){

  4. });
复制代码
现在如果你在项目文件夹的根目录下运行 mocha 命令,所有的东西都会通过。我们现在来想想我们的模块需要做些什么,我们想要给它传递命令参数数组来运行这个app,然后我们想要让它创建一个包含所有tags的对象,如果能有一个默认对象那就更好了,以便当设置不完整时,我们也可以让对象存储一些东西。
在处理tags的时候,一些app还提供了一些简写选项,例如一个字符。假设我们想要在设置搜索深度的时候指定 --depth=2 ,我们也可以设置为 -d=2
现在我们先来处理长格式标签(例如,’–depth=2’),我们编写第一个测试:
  1. describe('Tags',function(){
  2.     describe('#parse()',function(){
  3.         it('should parse long formed tags',function(){
  4.             var args = ['--depth=4','--hello=world'];
  5.             var results = tags.parse(args);
  6.             expect(results).to.have.a.property('depth',4);
  7.             expect(results).to.have.a.property('hello','world');
  8.         });
  9.     });
  10. });
复制代码
我们在测试套件中添加了一个叫做parse的方法,同时我们添加了一个用来测试长格式标签的测试。在这个测试中,我们创建了一个实例命令,同时为这个两个实例命令添加了两个断言。
现在运行Mocha,你应该会看到一个错误,错误显示tags没有一个叫做parse的函数。为了修复这个错误,我们来为tags模块添加一个parse函数。创建一个node模块的方式如下所示:
  1. exports = module.exports = {};

  2. exports.parse = function(){

  3. }
复制代码
错误显示我们需要一个parse方法,因此我们现在就来创建它,我们并不添加其他任何代码因为它没有告诉我们要这样做。通过编写最少量的代码,你可以确保不会编写用于通过测试的多余代码。
现在我们再次运行Mocha,这次我们将看到一个错误,错误告诉我们无法从一个未定义的变量中读取一个叫做depth的属性。这是因为当前我们的parse函数还没有返回什么东西,因此我们为它添加一些代码让他返回一个对象:
  1. exports.parse = function(){
  2.     var options = {};

  3.     return options;
  4. }
复制代码
我们正在慢慢向前,如果你再次运行Mocha,没有任何的异常被抛出,只有一个错误显示空对象中没有一个叫做depth的属性。

现在我们可以开始编写一些真实的代码了。因为我们的函数需要解析标签并将它添加到我们的对象中,我们需要循环参数数组并且移除双破折号。
  1. exports.parse = function(args) {
  2.     var options = {}
  3.     for (var i in args) { //Cycle through args
  4.         var arg = args[i];
  5.         //Check if Long formed tag
  6.         if (arg.substr(0, 2) === "--") {
  7.         arg = arg.substr(2);
  8.         //Check for equals sign
  9.         if (arg.indexOf("=") !== -1) {
  10.             arg = arg.split("=");
  11.             var key = arg.shift();
  12.             options[key] = arg.join("=");
  13.         }
  14.     }
  15.    }
  16.    return options
复制代码
这段代码循环了参数数列,确保了我们可以处理一个长格式标签,然后我们通过第一个等号将这个字符串拆开以便在options对象汇总创建一个键值对。
现在我们马上就能够解决问题了,但是如果我们再次运行Mocha,我们会看到我们现在有了一个叫做depth的键,但是它是一个字符串而不是一个数字。数字能使我们更加轻松的编写这个app,因此在下一块的代码中我们需要将这个值转换为数字。我们可以用一些正则表达式和parseInt函数来解决这个问题:
  1. if (arg.indexOf("=") !== -1) {
  2.     arg = arg.split("=");
  3.     var key = arg.shift();
  4.     var value = arg.join("=");
  5.     if (/^[0-9]+$/.test(value)) {
  6.         value = parseInt(value, 10);
  7.     }
  8.     options[key] = value;
  9. }
复制代码
现在再次运行Mocha,你应该能通过一个测试。数字转换在我们的测试中应该是广泛讯在的,或者至少在测试的声明中有提到因此你不会错误的移除数字转换断言;因此我们需要在it声明中添加“add and convert numbers”或者将它分开成为一个新的it块。这取决于你是怎么考虑“明显的默认行为”或者一个分离的特性。

当你看到一个通过的spec时,你应该编写更多的测试。我们想要做的下一件事情就是添加默认字符串,因此在tagSpec文件内再次添加一个it块:
  1. it("should parse long formed tags and convert numbers", function(){
  2.     var args = ["--depth=4", "--hello=world"];
  3.     var results = tags.parse(args);
  4.     expect(results).to.have.a.property("depth", 4);
  5.     expect(results).to.have.a.property("hello", "world");
  6. });
  7. it("should fallback to defaults", function(){
  8.     var args = ["--depth=4", "--hello=world"];
  9.     var defaults = { depth: 2, foo: "bar" };
  10.     var results = tags.parse(args, defaults);
  11.     var expected = {
  12.         depth: 4,
  13.         foo: "bar",
  14.         hello: "world"
  15.     };
  16.     expect(results).to.deep.equal(expected);
  17. });
复制代码
现在我们使用了一个新的测试,deep equal是一个比较相等值的好方法。另外,你还可以使用eql测试,但是deep equal更加清晰一点。这个测试传递两个参数作为命令字符串并传递两个默认值来进行覆盖,这样做的话我们可以在测试用例中获得更好的延伸。
再次运行Mocha,你应该看到一点不同的地方,它包含了期望值和实际值之间的差别。

现在我们回到tag.js模块,我们来添加这个功能。这非常的容易,我们仅仅需要接受第二个参数,当它被设置为一个对象时,我们可以用标准的空对象来替换它:
  1. exports.parse = function(args, defaults) {
  2.    var options = {};
  3.    if (typeof defaults === "object" && !(defaults instanceof Array)) {
  4.     options = defaults
  5.    }
复制代码
这样编写代码会使得测试通过。我们想要做的下一件事情是能够添加一个标签而不给它指明一个值,让它像一个布尔值一样工作。例如,如果我们设置searchContents 或者其它类似的东西,它就会把这个选项添加进对象中并设置为true。
测试如下所示:
  1. it("should accept tags without values as a bool", function(){
  2.     var args = ["--searchContents"];
  3.     var results = tags.parse(args);

  4.     expect(results).to.have.a.property("searchContents", true);
  5. });
复制代码
运行这个测试会得到和上面一样的错误:

在for循环内部,当我们获得一个长格式标签的匹配,我们会检查其中是否包含一个等号,我们可以为这个测试代码简单的添加一个else语句来将值设置为true:
  1. if (arg.indexOf("=") !== -1) {
  2.     arg = arg.split("=");
  3.     var key = arg.shift();
  4.     var value = arg.join("=");
  5.     if (/^[0-9]+$/.test(value)) {
  6.         value = parseInt(value, 10);
  7.     }
  8.     options[key] = value;
  9. } else {
  10.     options[arg] = true;
  11. }
复制代码
我们要做的下一件事情是添加端标签。它将是parse函数的第三个标签,基本上来说是一个包含着字母和它们相应替换值的对象。下面是这个测试的spec:
  1. it("should accept short formed tags", function(){
  2.      var args = ["-sd=4", "-h"];
  3.      var replacements = {
  4.         s: "searchContents",
  5.         d: "depth",
  6.         h: "hello"
  7.      };
  8.      var results = tags.parse(args, {}, replacements);
  9.          var expected = {
  10.          searchContents: true,
  11.          depth: 4,
  12.          hello: true
  13.      };
  14.      expect(results).to.deep.equal(expected);
  15. });
复制代码
标签带来的麻烦是它们可以合并成一行。这句话的意思是它不像长标签一样是彼此分开的,短标签 - 只是单个字符 - 你可以用 -vgh 来表示三个不同属性。这使得解析变得有些困难了,因为我们依然需要允许使用等号来为最后一个标签添加值。同时你还需要注册其他的标签。但是不要担心,只要使用足够的pop和shift就可以解决这个问题:
下面是完整版的parse函数:
  1. exports.parse = function(args, defaults, replacements) {
  2.     var options = {};
  3.     if (typeof defaults === "object" && !(defaults instanceof Array)) {
  4.         options = defaults
  5.     }
  6.     if (typeof replacements === "object" && !(defaults instanceof Array)) {
  7.         for (var i in args) {
  8.             var arg = args[i];
  9.             if (arg.charAt(0) === "-" && arg.charAt(1) != "-") {
  10.                arg = arg.substr(1);
  11.                if (arg.indexOf("=") !== -1) {
  12.                     arg = arg.split("=");
  13.                     var keys = arg.shift();
  14.                     var value = arg.join("=");
  15.                     arg = keys.split("");
  16.                     var key = arg.pop();
  17.                     if (replacements.hasOwnProperty(key)) {
  18.                           key = replacements[key];
  19.                     }
  20.                    args.push("--" + key + "=" + value);
  21.               } else {
  22.                   arg = arg.split("");
  23.               }
  24.              arg.forEach(function(key){
  25.                   if (replacements.hasOwnProperty(key)) {
  26.                        key = replacements[key];
  27.                   }
  28.                  args.push("--" + key);
  29.             });
  30.         }
  31.     }
  32.    }
复制代码
和前面相比,这段代码太多了,但是我们真正做的事情是通过等号将参数分开,然后将键分开为单个字母。因此在例子中如果我们传递了 -gj=asd ,那么我们会将asd 分到一个叫做 value 的变量中,然后我们需要将 gj 部分分成单个字母。最后一个字母(在本例中是j)将会成为成为值(asd)的键,然而任何在它之前的字母,将会被添加正常的布尔值标签。我们现在并不想处理这些标签,以防我们在稍后要改变它。因此我们现在做的仅仅是将短标签转换成为长标签,然后让我们的脚本来处理它。
再次运行Mocha我们将得到四个绿色的结果,这证明四个测试都通过了。
现在我们还有一些东西可以添加到tags模块中以便使它更接近一个npm模块,像是能够存储普通文本参数,比如注释的能力,或者能够收集所有末尾的文本用于查询的能力。但是这篇文章已经很长了,所以我们接下来要实现的搜索功能。
Search模块
我们刚刚使用TDD一步一步创建了一个模块,你应该已经对TDD有了一些了解。在后面的文章中,我们将加快测试的速度,将代码合并起来并且只展示最终结果。
首先在lib文件夹内部创建一个search.js文件,同时在test文件夹中创建一个searchSpec.js文件。
接下来打开这个spec文件,我们开始编写第一个测试,我们需要它能够基于一个depth参数来得到一个文件列表,这也能很好的证明代码可以通过来自外部的设置运行。当处理外部类似于对象的数据文件,或者在我们的样例文件中时,你会想要进行一些预先设置,它用来和你的测试一起运行,但是你不想在你的系统中添加一些错误的信息。
解决问题的方法有两种,你可以伪造数据(mock the data),就像前面提到的那样,如果你正在处理语言本身用于载入数据的命令,你完全不需要去测试它们。在这种情况下,你可以简单地提供一些“提取出来的”数据然后继续你的测试,就像是我们在tags库中对命令字符串做的那样。但是在这里的情形中,我们测试的是我们添加到语言文件读取能力中的递归功能,这依赖于具体的层数。像这样的情形,你确实需要编写一个测试,因此我们需要创建一些实力文件来测试文件读取。另一种替代的方法是替换掉fs函数让它只运行不做别的事,然后我们就可以数出我们的假函数运行了多少次或者类似于这样的事情。但是在这里的例子中,我们还是要来床架你一些文件。
Mocha提供了一些函数,它们会在你的测试之前或之后运行,因此你可以将外部设置或者清空的工作在这些函数中完成。
例如,我们将要创建几个测试文件以及几个不同程度的文件夹,因此我们的测试代码如下所示:
  1. var expect = require("chai").expect;
  2. var search = require("../lib/search.js");
  3. var fs = require("fs");
  4. describe("Search", function(){
  5.     describe("#scan()", function(){
  6.         before(function() {
  7.             if (!fs.existsSync(".test_files")) {
  8.                 fs.mkdirSync(".test_files");
  9.                 fs.writeFileSync(".test_files/a", "");
  10.                 fs.writeFileSync(".test_files/b", "");
  11.                 fs.mkdirSync(".test_files/dir");
  12.                 fs.writeFileSync(".test_files/dir/c", "");
  13.                 fs.mkdirSync(".test_files/dir2");
  14.                 fs.writeFileSync(".test_files/dir2/d", "");
  15.             }
  16.         });
  17.         after(function() {
  18.             fs.unlinkSync(".test_files/dir/c");
  19.             fs.rmdirSync(".test_files/dir");
  20.             fs.unlinkSync(".test_files/dir2/d");
  21.             fs.rmdirSync(".test_files/dir2");
  22.             fs.unlinkSync(".test_files/a");
  23.             fs.unlinkSync(".test_files/b");
  24.             fs.rmdirSync(".test_files");
  25.         });
  26.      });
  27. });
复制代码
这些代码将会基于它们所在的describe块运行,你甚至可以使用beforeEach或者afterEach函数来让它们在所有的it块之前或之后运行。这些函数本身仅仅是使用了标准的Node命令来创建或移除文件。下面我们将要编**正的测试。它们将卸载after函数之后,但是依然在describe块内部:
  1. it("should retrieve the files from a directory", function(done) {
  2.     search.scan(".test_files", 0, function(err, flist){
  3.         expect(flist).to.deep.equal([
  4.             ".test_files/a",
  5.             ".test_files/b",
  6.             ".test_files/dir/c",
  7.             ".test_files/dir2/d"
  8.         ]);
  9.         done();
  10.     });
  11. });
复制代码
这是我们用于测试一个异步函数的第一个例子,但是正如你所见,它并不像前面那样简单,我们要做的是使用在it申明中Mocha提供的done函数,以便告诉它什么时候结束测试。
如果你在回调中指明了done变量,Mocha会自动为你检测它。它会等待你的调用,这允许你非常轻松的测试异步代码。另外,值得注意的一点是这种模式在Mocha的自始至终都可以使用,你可以在before和after中使用这种模式,如果你需要进行异步设置的话。
接下来,我们将编写一个测试来确保depth能够正常运行,如果它设置了的话:
  1. it("should stop at a specified depth", function(done) {
  2.     search.scan(".test_files", 1, function(err, flist) {
  3.         expect(flist).to.deep.equal([
  4.             ".test_files/a",
  5.             ".test_files/b",
  6.          ]);
  7.         done();
  8.     });
  9. });
复制代码
这里没有什么不同,仅仅是一个普通的测试。在Mocha中运行这个测试,我们会看到一个错误,错误显示search没有任何方法,这是因为我们还什么都没编写。因此现在我们来添加一些函数:
  1. var fs = require("fs");

  2. exports = module.exports = {};

  3. exports.scan = function(dir, depth, done) {

  4. }
复制代码
如果你再次运行Mocha,它将会暂停等待异步函数的返回,但是由于我们完全没有调用回调函数,这个测试将会超时。默认情况下两秒钟后将会超时,但是你可以通过在describe中或者block中使用 this.timeout(milliseconds) 来调整超时的时间。
这个scan函数接收一个路径和一个深度,同时返回一个它找到的文件的列表。当你开始思考我们怎样在一个单独的函数中递归两个不同的函数时,这里其实有一个小技巧。我们需要在不同的文件夹中进行递归,然后这些文件夹需要扫描自己来决定是否继续。
在这里同步的代码非常的轻松因为你可以一步一步的前进,每次完成一点。当你开始处理异步版本的时候,事情会变得有些复杂了,你不能仅仅只是做一个forEach循环或者其他事情,因为它在文件夹之间并不会暂停,它们本质上来说是同时开始运行并且各自返回不同的值,它们有可能会互相重写。
为了让它运行起来,你需要创建一些栈,以便你可以异步的每次处理一个(如果你使用队列的话可以一次处理全部),同时保持顺序。它不仅仅用来载入文件,基本上所有使用异步函数处理一个对象数组的情况都可以使用这种方法。
除此之外,还有一点需要申明,因为我们有一个depth选项,这个depth选项如何运行决定了你想要检查多少层级的文件夹。
下面是一个完整的函数代码:
  1. exports.scan = function(dir, depth, done) {
  2.     depth--;
  3.     var results = [];
  4.     fs.readdir(dir, function(err, list) {
  5.         if (err) return done(err);
  6.         var i = 0;
  7.         (function next() {
  8.             var file = list[i++];
  9.             if (!file) return done(null, results);
  10.             file = dir + '/' + file;
  11.             fs.stat(file, function(err, stat) {
  12.                  if (stat && stat.isDirectory()) {
  13.                     if (depth !== 0) {
  14.                         var ndepth = (depth > 1) ? depth-1 : 1;
  15.                         exports.scan(file, ndepth, function(err, res) {
  16.                             results = results.concat(res);
  17.                             next();
  18.                         });
  19.                      } else {
  20.                         next();
  21.                      }
  22.                  } else {
  23.                      results.push(file);
  24.                      next();
  25.                  }
  26.             });
  27.         })();
  28.     });
  29. };
复制代码

运行Mocha,现在所有的测试应该都能通过。我们需要实现的最后一个函数接受一个包含路径以及search关键字的数组,同时返回所有的匹配项。下面是关于它的测试:
  1. describe("#match()", function(){
  2.     it("should find and return matches based on a query", function(){
  3.         var files = ["hello.txt", "world.js", "another.js"];
  4.         var results = search.match(".js", files);
  5.         expect(results).to.deep.equal(["world.js", "another.js"]);
  6.         results = search.match("hello", files);
  7.         expect(results).to.deep.equal(["hello.txt"]);
  8.     });
  9. });
复制代码
最后,我们需要将match函数添加到我们的search.js文件中:
  1. exports.match = function(query, files){
  2.     var matches = [];
  3.     files.forEach(function(name) {
  4.         if (name.indexOf(query) !== -1) {
  5.             matches.push(name);
  6.         }
  7.     });
  8.     return matches;
  9. }
复制代码
为了确认结果,再次运行Mocha。你应该看到所有的七个测试都通过了。

合并起来
最后一步要编写的就是粘合代码,它会将我们所有的模块都合并起来,因此在我们的项目的根目录下添加一个叫做app.js的文件或者类似名字的文件,然后将下面的代码添加到其中:
  1. # !/usr/bin/env node

  2. var tags = require("./lib/tags.js");
  3. var search = require("./lib/search.js");
  4. var defaults = {
  5.     path: ".",
  6.     query: "",
  7.     depth: 2
  8. }
  9. var replacements = {
  10.     p: "path",
  11.     q: "query",
  12.     d: "depth",
  13.     h: "help"
  14. }

  15. tags = tags.parse(process.argv, defaults, replacements);

  16. if (tags.help) {
  17.     console.log("Usage: ./app.js -q=query [-d=depth] [-p=path]");
  18. } else {
  19.     search.scan(tags.path, tags.depth, function(err, files) {
  20.         search.match(tags.query, files).forEach(function(file){
  21.             console.log(file);
  22.         });
  23.     });
  24. }
复制代码
这里其实没有什么逻辑,我们要组的仅仅是将不同的模块方法联系到一起来得到我们想要的结果。我们并不需要测试这段代码,因为它仅仅是粘合代码,已经经过完整的测试。
我们现在确保我们的脚本文件是可执行的(在Unix系统下运行chmod +x app.js)然后运行下面的代码:
  1. ./app.js -q=".js"
复制代码
你尅自定义其他我们设置的占位。

总结
在本文中,我们创建了一个完整的文件搜索app,虽然是一个很简单的app,但是它完整的向我们展示了TDD的全过程。
最后我们还有一些建议:如果你将要进行很多TDD,先设置好你的环境。人们在进行TDD的时候,有很多时间都花在切换窗口,打开关闭文件,然后运行测试,一天的时间中可能会做好几百次这样的事情。这中情况严重的印象了你的工作流程,降低的效果。但是如果你提前设置好的编辑器,像是你可以一边编辑一边测试或者你的编辑器支持前后跳转,都将会为你省下大量的时间。你也可以自动运行你的测试,只需要再运行Mocha的时候加上 -w 标签,它可以见识你的文件变化并自动运行测试。这些准备工作都将使你的工作流程更加顺畅并减少你工作中的困扰。
本文译自Testing in Node.js,原文地址http://code.tutsplus.com/tutorials/testing-in-node-js–net-35018
    作者:佚名

HTML5中国微信

小黑屋|关于我们|HTML5论坛|友情链接|手机版|HTML5中国 ( 京ICP备11006447号 京公网安备:11010802018489号  

GMT+8, 2017-6-24 20:25

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表