JavaScript基础教程 用 Mocha 和 Chai 对 JavaScript 代码进行单元测试
沉沙 2018-07-23 来源 : 阅读 1298 评论 0

摘要:本篇JavaScript教程探讨了用 Mocha 和 Chai 对 JavaScript 代码进行单元测试,希望阅读本篇文章以后大家有所收获,帮助大家对JavaScript的理解更加深入。

你曾试过修改代码后,导致其它地方出现问题吗?

我相信很多人都遇到过。因为这是几乎不可避免的,特别在庞大的代码面前。由于代码间可能是环环相扣的,改变一处会影响另一处。

但如果这种情况不会发生呢?如果有一种方法能让你知道改变后会出现的结果呢?这无疑是极好的。因为修改代码后无需担心会破坏什么东西,从而程序出现 bug 的概率更低,在 debug 上花费时间更少。

这就是单元测试的魅力。它能自动检测代码中的任何问题。在修改代码后进行相应测试,若有问题,能立刻知道问题是什么,问题在哪和正确的做法是什么。这完全可以消除任何猜测!

在本文,我会让你了解如何对 JavaScript 代码进行单元测试。而且,在本文出现的案例和技术可同时应用到基于浏览器的代码和 Node.js 的代码。

什么是单元测试

当你对代码库进行测试时,可先取一段代码(通常是一个函数),然后在特定情况下,验证其行为是否正确。而单元测试就是这方面的一种结构化和自动化的方法。当然,写的测试越多,获得的益处也更大。这也会让你在开发时更加自信。

单元测试的核心思想是给函数特定的输入值,测试其行为。也就是说,以特定的参数调用函数,然后检查是否得到正确的结果。

// 输入 1 和 10...

var result = Math.max(1, 10);

 

// ...应该输出 10

if(result !== 10) {

  throw new Error('Failed');

}

   

在实际中,测试有时会更复杂。例如,如果你的函数含有一个 Ajax 请求,那么测试就需要设定更多的东西。当然,“根据特定的输入值得到特定的输出值”原理仍然适用。

设置工具

在本文,我们选择 Mocha。它入门简单,能同时适用于基于浏览器的测试和 Node.js 的测试,而且与其它测试工具配合同样运行良好。

安装 Mocha 的最简单方式是使用 npm(为此,也需要安装 Node.js)。如果你不懂得如何在你的电脑上安装 npm 或 Node.js,可查看我的教程 A Beginner’s Guide to npm — the Node Package Manager。

安装好 Node.js 后,在你的项目目录下打开 terminal 或 command line。

· 如果你想在浏览器上测试代码,执行 npm install mocha chai --save-dev。

· 如果你想测试 Node.js 代码,除了执行上面那行命令,也要执行 npm install -g mocha。

此时已经安装了 mocha 和 chai 包(package)。Mocha 是一个运行测试的库,而 Chai 包含一些有用的功能,我们能利用这些功能对我们的测试结果进行验证。

Node.js vs Browser 测试对比

下面的案例是在浏览器上运行测试的。如果想为你的 Node.js 应用进行单元测试,要遵循以下步骤。

· 对于 Node,无需测试运行文件(test runner file)。

· 为了引入 Chari,需在测试文件顶部添加语句 var chai = require('chai');。

· 用 mocha 命令执行单元测试,而不是打开浏览器。

设置目录结构

为了让文件结构更清晰,应将测试文件放在主代码文件的一个独立目录下。这是为了方便以后添加其它类型的测试(如集成测试(integration tests) 和 功能测试(functional tests))。

对于 JavaScript,最流行的实践方案是在项目根目录下创建一个 test/ 文件夹。然后,将每个测试文件放置在该文件夹下,如 test/someModuleTest.js。另一种方案是,在 test/ 目录下,再创建文件夹。但我建议尽量保持简单——这样能保证在后面必要时进行(快速)修改。

设置测试运行器(Test Runner)

为了能在浏览器上进行测试,我们需要创建一个简单的 HTML 页面作为测试运行页(test runner page)。该页面会加载 Mocha、测试库文件和实际测试文件。为了运行这些测试,我们只需在浏览器打开运行器(runner)。

如果你使用 Node.js,你可跳过这一步。Node.js 的单元测试能通过命令 mocha 运行,前提是按照我推荐的目录结构。

下面是我们用于测试运行器(test runner)的代码。我将其存为 testrunner.html。

<!DOCTYPE html>

<html>

  <head>

    <title>Mocha Tests</title>

    <link rel="stylesheet" href="node_modules/mocha/mocha.css">

  </head>

  <body>

    <div id="mocha"></div>

    <script src="node_modules/mocha/mocha.js"></script>

    <script src="node_modules/chai/chai.js"></script>

    <script>mocha.setup('bdd')</script>

 

    <!-- load code you want to test here -->

 

    <!-- load your test files here -->

 

    <script>

      mocha.run();

    </script>

  </body>

</html>

   

该测试运行器的几个重要点:

· 为了让测试结果拥有漂亮的样式,我们加载了 Mocha 的 CSS 文件。

· 创建了一个 ID 为 mochat 的 div 标签。测试结果将放在该标签内。

· 加载 Mocha 和 Chai 脚本文件。由于这两个文件是通过 npm 安装的,它们被放在 node_modules 目录的子文件夹下。

· 通过调用 mocha.setup,开启 Mocha 的测试功能(testing helpers)。

· 然后,加载需要的测试项和相应测试的文件。尽管我们还没在这放置任何代码。

· 最后,调用了 mocha.run 执行相应测试。当然,要确保在资源和测试文件加载完成后再调用该函数。

基本的测试骨架

现在我们可以运行测试了,下面就开始写点测试相关的东西吧。

首先,创建 test/arrayTest.js。每个文件名都有其具体含义,显然它是个测试文件,并会测试 array 的基本功能。

每个测试案例文件都会遵循以下基本模式。首先,有个 describe 块:

describe('Array', function() {

  // Further code for tests goes here

});

   

describe 用于把单独的测试聚合在一起。其第一个参数用于指示测试什么。在本例中,由于我们打算测试 array 功能,我传入一个 'Array' 字符串。

然后,在 describe 内需有 it 块:

describe('Array', function() {

  it('should start empty', function() {

    // Test implementation goes here

  });

 

  // We can have more its here

});

   

it 用于创建实际的测试。其第一个参数是对该测试的描述,且该描述的语言应该是人类可读的(而非编程语言)。如在本例中,“it should empty”能很好地描述了 array 的行为。实现该测试的具体代码则写在 it 的第二个参数 function 内。

所有 Mocha 测试都以同样的骨架编写,而且它们遵循相同的基本模式。

· 首先,使用 describe 表明我们测试什么,如“描述 array 该如何运行”。

· 然后,使用多个 it 函数创建独立的测试,每个 it 应该描述一个特定的行为,如上述的案例 “it should start empty(array 运行前应为空)”

编写测试代码

现在我们已经知道如何构造测试案例了,下面就开始更有趣的部分——实现测试。

由于我们的测试是 array 初始值应为空,即我们需要创建一个数组并确保它为空。实现该测试是非常简单的:

var assert = chai.assert;

 

describe('Array', function() {

  it('should start empty', function() {

    var arr = [];

 

    assert.equal(arr.length, 0);

  });

});

   

请注意首行代码,我们设置了 assert 变量。这样就不用每次都输入 chai.assert 了。

在 it 函数里,我们创建了一个数组并检查其长度。尽管简单,但很好地展示了测试是如何工作的。

首先,你有东西需要被测试——这叫 被测系统(System Under Test,SUT)。若有需要,则对被测系统进行相应操作。对于上述案例,由于检查数组初始值是否为空,我们没做任何操作。

测试的最后步骤应该是验证——对结果进行断言(assertion)检查。对于上述案例,我们对此使用 assert.equal。大多数断言函数的参数顺序是一致的:首先是“实际”值,然后是“期待”值。

实际值是测试代码的结果,因此,在该案例中是 arr.length。

期待值是预想的结果。由于数组的初始值应为空,因此,在该案例中的期待值是 0。

虽然 Chai 提供了两种不同的断言(assertion)编写方式,但现在为了保持简单,我们使用了 assert。当你能熟练编写测试时,你可能更想用 expect assertions ,因为它提供了更灵活的操作。

运行测试

为了运行该测试,我们需要将其添加到先前创建的测试运行器文件内。

对于 Node.js,可跳过此步骤,然后使用命令 mocha 执行测试。你会在 terminal 里看到测试结果。

向运行器添加该测试(针对浏览器端):

<!-- load your test files here -->

<script src="test/arrayTest.js"></script>

   

你一旦添加了脚本,就可以加载测试运行器页面了(若选择在浏览器进行测试)。

测试结果

当你运行这些测试,其测试结果看起来和下图类似:

 

注意:在 describe 和 it 函数的描述语句都在页面展示出来了——测试项(如:should start empty)都分组放在描述(如:Array)下。当然,也可以对 describe 块再嵌套,以创建更深的子分组。

下面看看测试失败会显示什么。

将测试的该行代码进行修改:

assert.equal(arr.length, 0);

   

将 0 改为 1。这无疑会导致测试失败,因为数组长度不再匹配期待值。

如果你再次运行测试,那么在测试结果中,运行错误的描述将以红色显示。

 

测试的一项好处是能帮助你更快地找到 bug,尽管错误信息在这并不是非常详细。但是我们可以解决这个问题。

大多数断言函数都带有一个可选的 message 参数。该信息参数会在断言失败时显示。因此我们可以利用该参数,让错误信息更容易理解。

我们能像下面那样向断言添加 message 参数:

assert.equal(arr.length, 1, 'Array length was not 0');

   

如果你再次运行测试,那么自定义的信息会取代默认的信息而显示出来。

OK,让我们将 1 改回 0,确保测试通过。

综合案例

到目前为止,案例都是相当简单的。那么下面就让我们将学到的知识付诸实践,看看如何测试一段实际当中所用到的代码。

下面是一个将 CSS 类名添加到元素的函数。我们将该函数放进新文件 js/className.js。

function addClass(el, newClass) {

  if(el.className.indexOf(newClass) === -1) {

    el.className += newClass;

  }

}

   

当元素的 className 属性不含有新类名时,才向元素添加新类名——毕竟谁想看到 <div class="hello hello hello hello">。

在最好的情况下,我们要在编写代码前先为该函数编写测试。但 测试驱动开发(test-driven development) 是一个复杂的主题,因此我们现在仅专注于编写测试。

开始前,让我们重温单元测试的基本思想:赋予函数特定的输入值,然后验证函数的行为是否符合预期。所以,该函数的输入值和行为是什么呢?

给定一个元素和一个类名:

· 若元素的 className 属性未含有该类名,则应添加。

· 若元素的 className 属性已含有该类名,则不应添加。

将这两种情况转化为两个测试。在 test 目录下,创建新文件 classNameTest.js 并添加以下内容:

describe('addClass', function() {

  it('should add class to element');

  it('should not add a class which already exists');

});

   

我们也可以将措词稍微地改成“it should do X”,虽然可读性更强一点,但本质上仍然与我们上述语句的可读性一致。根据原来的措词联想到相应的测试也不难。

等等,测试函数跑去哪了?当我们省略 it 的第二个参数,Mocha 会在测试结果中标记这些测试为待测试项。这让设置多个测试变得更方便——就像一个备忘录,列着打算编写的测试。

接着实现第一个测试。

describe('addClass', function() {

  it('should add class to element', function() {

    var element = { className: '' };

 

    addClass(element, 'test-class');

 

    assert.equal(element.className, 'test-class');

  });

 

  it('should not add a class which already exists');

});

   

在该测试中,我们创建了 element 变量,并将其与字符串 test-class(作为元素的新类名) 作为参数传入 addClass 函数。然后,使用断言检查该类名是否已包含在值(element.className)里。

再一次,我们从初始的想法出发——给定一个元素和一个类名,将类名添加到 class 列表,然后以简单的方式将其转化为代码。

尽管该函数(addClass)是针对 DOM 元素的,但我们在此使用了一个简单 JS 对象(plain JS object,根据 jQuery 官方定义:含有零个或多个键值对的对象)。是的,有时我们可以利用 JavaScript 的动态特性,以上述方式简化测试。如果不这样做,我们就要创建一个实际的元素,这无疑会使测试代码变复杂。当然,这还有另一个好处,由于没使用 DOM,该测试也能在 Node.js 运行。

在浏览器运行测试

为了在浏览器运行测试,你需要在运行器添加 className.js 和 classNameTest.js。

<!-- load code you want to test here -->

<script src="js/className.js"></script>

 

<!-- load your test files here -->

<script src="test/classNameTest.js"></script>

   

正如下面 CodePen 中所显示的:一个测试通过,而另一个显示待测试。注意:为了让代码运行在 CodePen 环境下,代码需稍作调整。

See the Pen Unit Testing with Mocha (1) by SitePoint (@SitePoint) on CodePen.

接着,实现第二个测试…

it('should not add a class which already exists', function() {

  var element = { className: 'exists' };

 

  addClass(element, 'exists');

 

  var numClasses = element.className.split(' ').length;

  assert.equal(numClasses, 1);

});

   

经常运行测试是一种好习惯。因此,让我们现在运行测试看看会发生什么。

不出所料,两者均通过。

下面是在 CodePen 中实现第二个测试的例子。

See the Pen Unit Testing with Mocha (2) by SitePoint (@SitePoint) on CodePen.

但事情没那么简单!该函数的第三种情况我们并没有考虑到,这也是该函数的一个非常严重的 Bug。虽然该函数只有三行代码,但你注意到了吗?

下面为第三种情况编写多一个案例,让这个 Bug 暴露出来。

it('should append new class after existing one', function() {

  var element = { className: 'exists' };

 

  addClass(element, 'new-class');

 

  var classes = element.className.split(' ');

  assert.equal(classes[1], 'new-class');

});

   

你可在下面的 CodePen 中看到,这次测试失败了。导致该问题的原因很简单:元素上的 CSS 类名应以空格隔开。然而,现在实现的 addClass 并未加空格!

See the Pen Unit Testing with Mocha (3) by SitePoint (@SitePoint) on CodePen.

修复该函数,让测试通过。

function addClass(el, newClass) {

  if(el.className.indexOf(newClass) !== -1) {

    return;

  }

 

  if(el.className !== '') {

    //ensure class names are separated by a space

    newClass = ' ' + newClass;

  }

 

  el.className += newClass;

}

   

修复后,最终在 CodePen 测试通过。

See the Pen Unit Testing with Mocha (4) by SitePoint (@SitePoint) on CodePen.

在 Node 中运行测试

在 Node 中,只有同一文件中的内容是可见的。由于 className.js 和 classNameTest.js在不同文件下,我们需要一种方式将一个文件导出到另一个文件内。而标准的方式是通过 module.exports。如果你需要复习相关知识,你可以看看 Understanding module.exports and exports in Node.js。

代码本质不变,只是结构稍微不同:


// className.js

 

module.exports = {

  addClass: function(el, newClass) {

    if(el.className.indexOf(newClass) !== -1) {

      return;

    }

 

    if(el.className !== '') {

      //ensure class names are separated by a space

      newClass = ' ' + newClass;

    }

 

    el.className += newClass;

  }

}

 

// classNameTest.js

 

var chai = require('chai');

var assert = chai.assert;

 

var className = require('../js/className.js');

var addClass = className.addClass;

 

// 文件其它部分保持不变

describe('addClass', function() {

  ...

});

   

正如你所看到的,测试通过。

 

本文由职坐标整理发布,更多相关内容,请关注职坐标WEB前端JavaScript频道!

本文由 @沉沙 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程