使用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閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容