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

推荐阅读更多精彩内容