使用mocha,chai和sinon测试你的前端js代码

特别声明:此篇文章是通过Nicolas的博文Testing your frontend JavaScript code using mocha, chai, and sinon进行翻译,整个译文带有我自己的理解与思想。如需转载此译文,需注明英文出处:https://nicolas.perriault.net/code/2013/testing-frontend-javascript-code-using-mocha-chai-and-sinon/以及作者相关信息
——作者:Nicolas
——译者:�Chester723


随着网络应用的丰富度与复杂程度的日益增长,如果你要同时保持一颗清醒的头脑来应对,你就需要利用单元测试来检验你的前端js代码

在过去的四个月里,我一直在为Mozilla做着某个大项目,其中也不乏遇到过有关单测的一些技巧。虽然我们有尝试在这方面使用CasperJS来实现,但是Firefox在当时并不被支持而且我们还需要考虑到js引擎的兼容性问题,为此,我们放弃了它转而使用Mocha, Chai, Sinon这三个被认为目前最好的单测工作流。

mocha测试框架与chai expectation库

Mocha本身是一个单元测试框架而chai则是一个被称之为期望函数的库。为了能够更直观的说明两者的关系,我们可以把mocha比作一个用于描述单元测试的组件,而chai则可以通过便利的辅助函数设立断言来判断我们的代码执行结果是否有达到预期。

例如,现在我们有一个Cow对象要被用于单元测试:

// cow.js
(function(exports) {
  "use strict";

  function Cow(name) {
    this.name = name || "Anon cow";
  }
  exports.Cow = Cow;

  Cow.prototype = {
    greets: function(target) {
      if (!target)
        throw new Error("missing target");
      return this.name + " greets " + target;
    }
  };
})(this);

乍这么看上去好像没什么特别的地方,但我们依旧要对它测试一下。

mocha和chai都可以被用在node环境与浏览器环境里,对于后者而言,我们需要构建一个测试用的html页面并配合�以下几个库:

我个人的建议是把这些第三方的库都放在一个叫做vendor的子文件夹里。让我们来构建一个html文件来测试我们的代码:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Cow tests</title>
  <link rel="stylesheet" media="all" href="vendor/mocha.css">
</head>
<body>
  <div id="mocha"><p><a href=".">Index</a></p></div>
  <div id="messages"></div>
  <div id="fixtures"></div>
  <script src="vendor/mocha.js"></script>
  <script src="vendor/chai.js"></script>
  <script src="cow.js"></script>
  <script>mocha.setup('bdd')</script>
  <script src="cow_test.js"></script>
  <script>mocha.run();</script>
</body>
</html>

请注意这里我们将使用到Chai's BDD Expect API这种方式,因此我们会有类似mocha.setup('bdd')这样的调用。

现在让我们为之前的Cow对象写一个简单的测试组件并把它保存到cow_test.js文件中:

//cow_test.js
var expect = chai.expect;

describe("Cow", function() {
  describe("constructor", function() {
    it("should have a default name", function() {
      var cow = new Cow();
      expect(cow.name).to.equal("Anon cow");
    });

    it("should set cow's name if provided", function() {
      var cow = new Cow("Kate");
      expect(cow.name).to.equal("Kate");
    });
  });

  describe("#greets", function() {
    it("should throw if no target is passed in", function() {
      expect(function() {
        (new Cow()).greets();
      }).to.throw(Error);
    });

    it("should greet passed target", function() {
      var greetings = (new Cow("Kate")).greets("Baby");
      expect(greetings).to.equal("Kate greets Baby");
    });
  });
});

不出什么意外,如果你把html文件在浏览器打开,你将看到一个测试通过的页面如下:

Paste_Image.png

如果有任何断言达不到期望,你会在测试结果中被提示,例如我们将greets函数稍微改写:

  Cow.prototype = {
    greets: function(target) {
      if (!target)
        throw new Error("missing target");
      return this.name + " greets " + target + "!";
    }
  };

你将会看到:

Paste_Image.png

那我们又如何测试异步调用的代码?

试想下我们有一个叫做Cow#lateGreets的函数,它会在一秒的延迟之后执行:

  Cow.prototype = {
    greets: function(target) {
      if (!target)
        throw new Error("missing target");
      return this.name + " greets " + target + "!";
    },

    lateGreets: function(target, cb) {
      setTimeout(function(self) {
        try {
          cb(null, self.greets(target));
        } catch (err) {
          cb(err);
        }
      }, 1000, this);
    }
  };

当然我们同样可以对它进行测试,mocha内部提供的done函数早就为我们准备好了一切:

describe("#lateGreets", function() {
    it("should pass an error if no target is passed", function(done) {
      (new Cow()).lateGreets(null, function(err, greetings) {
        expect(err).to.be.an.instanceof(Error);
        done();
      });
    });

    it("should greet passed target after one second", function(done) {
      (new Cow("Kate")).lateGreets("Baby", function(err, greetings) {
        expect(greetings).to.equal("Kate greets Baby");
        done();
      });
    });
  });

很方便的是,mocha会用红色的字标出那些可能被怀疑成超时操作的时间:

Paste_Image.png

使用Sinon来伪造或者模拟真实环境

当你在进行单元测试的时候,你可能不希望将其依赖于其他的类库,这种依赖性很可能会使你写的函数产生一定的副作用,而sinon在拥有存根模拟类库操作的两大功能的帮助下,可以极大的减少这种副作用。

举个栗子,我们试想下在之前的Cow#greets方法中,如果我们把原来本该在抛出异常返回字符串的情况改为通过调用window.console的error方法:

// cow.js
(function(exports) {
  "use strict";

  function Cow(name) {
    this.name = name || "Anon cow";
  }
  exports.Cow = Cow;

  Cow.prototype = {
    greets: function(target) {
      if (!target)
        return console.error("missing target");
      console.log(this.name + " greets " + target);
    }
  };
})(this);

这种情况我们又如何对其测试呢?好在sinon已经提供好了解决方案。首先,让我们先引入sinon的js代码:

<script src="vendor/mocha.js"></script>
<script src="vendor/chai.js"></script>
<script src="vendor/sinon-1.7.1.js"></script>

首先我们先暂存console对象的logerror方法,以确保我们是否调用了它们以及什么参数被传入进去:

var expect = chai.expect;

describe("Cow", function() {
  var sandbox;

  beforeEach(function() {
    // create a sandbox
    sandbox = sinon.sandbox.create();

    // stub some console methods
    sandbox.stub(window.console, "log");
    sandbox.stub(window.console, "error");
  });

  afterEach(function() {
    // restore the environment as it was before
    sandbox.restore();
  });

  // ...

  describe("#greets", function() {
    it("should log an error if no target is passed in", function() {
      (new Cow()).greets();

      sinon.assert.notCalled(console.log);
      sinon.assert.calledOnce(console.error);
      sinon.assert.calledWithExactly(console.error, "missing target")
    });

    it("should log greetings", function() {
      var greetings = (new Cow("Kate")).greets("Baby");

      sinon.assert.notCalled(console.error);
      sinon.assert.calledOnce(console.log);
      sinon.assert.calledWithExactly(console.log, "Kate greets Baby")
    });
  });
});

这里有几点需要留意:

  • beforeEachafterEach是mocha api的一部分,它们确保你可以在测试的开始与销毁的时候做一些相关的操作
  • sinon提供了沙盒机制,基本上你可以定义和附带一系列的存根到一个沙盒对象上,且该对象可以在某个时刻重置之前暂存过的函数方法
  • 当函数方法被暂存(stub),真实的方法不会再被调用,所以在浏览器里的控制台里显然不会有任何错误信息被打印出来
  • sinon内部自己封装了一套断言语法:sinon.assert,当然它也有专门为chai制定的插件:sinon-chai,有兴趣的可以去看下哈

有关mocha, chai和sinon的更多功能和技巧你可以关注它们各自的站点,在这篇文章里我无法全部覆盖,但我希望你可以带着一种求贤若渴的心态去对前端的单元测试一探究竟。愿单测愉快~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容