Javascript Unit Test: Mocha, Chai, Sinon

Evan
evan.fang
Published in
9 min readJan 27, 2019

單元測試是軟體開發的利器,是保護程式碼邏輯的手段,也是引導開發者撰寫出易於測試的程式碼的指南針。

完備的單元測試,意味著開發者可以大膽而順暢的重構程式碼,在一路綠燈的情況下,不斷地小規模地優化程式碼架構,而不用擔心改壞了什麼(至少有被測試所覆蓋的部分是安全的)。

若你接手了他人的專案,先執行一次單元測試,確認測試all pass後,再來仔細探究測試程式碼所揭示的系統邏輯,相信你會對此專案具有更好的信心。

本文將介紹關於Javascript單元測試的入門知識,將包含以下語言或工具:

  1. Node.js
  2. Mocha
  3. Chai
  4. Code coverage: Istanbul
  5. Sinon

首先試著建立第一個單元測試,先建立專案目錄結構。

mkdir mytestcd mytestnpm init -ymkdir test

建立被測程式檔與測試程式檔。

touch app.jstouch test/app.test.js

安裝mocha。

npm install mocha --save-dev

調整 package.json 的test script。

"scripts": {"test": "mocha"}

app.test.js 撰寫測試程式碼。

在command line中執行 npm test ,會看到如下輸出:

至此,完成了第一個測試案例。請妥善利用 describe分類你的測試,並在 it 中寫入意圖清晰的測試描述。

接下來,在 app.js 中新增一個計算加法的function,並使用 chai 取代assert來做BDD風格的斷言,提升斷言的可讀性。

安裝 chai

npm install chai --save-dev

編輯 app.test.js

以上是使用 should 來做斷言。 chai 除了 should 外,也可以使用 expectassert ,端看個人喜好。

使用Istanbul可以產生程式碼測試覆蓋率(Code coverage)的報告。

安裝Istanbul。

$ npm install --save-dev nyc

修改 package.json

{
"scripts":
{
"test": "nyc mocha"
}
}

app.js 新增一個減法的function,但不要替這個function做unit test。

minus: function(num1, num2) {  return num1 - num2;}

執行 npm test ,結果如下:

若想設定覆蓋率一旦低於多少百分比,否則拋出測試失敗的錯誤,也可以辦到。如設定測試覆蓋率必須有95%以上,調整test語法如下:

"test": "nyc check-coverage --lines 95 mocha"

雖然所有unit test pass,但測試結果為失敗,因覆蓋率66.67%不滿足所設定的標準95%。

也可以產出html報表:

"test": "nyc --reporter html --reporter=text mocha"

但有時候,我們不只是想測試結果,還想要測試執行的過程,這種時候又該怎麼處理呢?例如以下程式碼,我們想要驗證傳進 doSomethingcallback function真的有被執行到。

這個時候就可以利用 sinon 這個工具,來幫助我們有效率的完成任務。

sinon提供以下模組:

  1. Fake
  2. Spy
  3. Stub
  4. Mock

本文不會仔細探究sinon的詳細用法,只會簡單地介紹如何使用 spystub 來協助我們進行單元測試。

簡單地說, spy 像是function的監視者,他可以“盯”住某個function,看看它有沒有被呼叫,被呼叫了幾次,被以什麼樣的參數呼叫。而 stub 則是spy的進階版本,具備spy的一切能力,但又多了一項新能力:可以“替換”掉原有function的行為。當你使用spy的時候,原本的function還是會執行,但當你使用stub的時候,你可以讓原本的function做你想要它做的事情(當然你也可以選擇讓他走原本的路)。

首先,在測試程式碼中使用 spy 來協助我們“監視”callback function的行動。

line 10: 建立一個spy function物件

line 12: 我們把它當作一個普通的callback function傳進 app.doSomething

line 14: 驗證callback function被呼叫的次數

而sinon本身其實也有提供assertion。所以你也可以如此下斷言:

sinon.assert.calledOnce(spy);

然後再來試試 stub

假設有以下程式碼,會透過API從server上取得使用者資料並回傳給呼叫端。

測試程式碼如下:

這段測試程式碼只是簡單的測試 getUserData function確實可以從server上拿到資料並回傳。順帶一提,由於getUserData是一個非同步方法,所以測試程式碼也要記得加上 async 關鍵字(line 8)。

測試結果:

注意到這個測試花的時間似乎比之前的測試都要長,原因也很明顯:因為需要實際打API去遠端的Server拿資料。若這樣的測試不止一個,而是有數十個,甚至數百、數千個,每次執行測試的時間想必會非常可觀。另一方面,若測試時的網路斷線,或是遠端伺服器因故暫停服務,這隻測試就會fail。以上的種種現象,想必不是開發人員所樂見的。

而這些現象,是肇因於我們的測試依賴外部環境所致。

要思考的是,我們究竟是要測試什麼?

是網路連線嗎?是伺服器是否正常運作嗎?如果都不是,那就不要讓這些因素來影響測試的成敗。因此,我們要切斷測試程式對外部環境的依賴。就這一點, stub 可以幫上忙。

就單元測試而言,我們僅需要確認:

  1. 這個function有呼叫到正確的URL
  2. get url的的結果,確實有被回傳

這兩件事情,才是真正決定 getUserData 這個function是否表現正常的關鍵。所以我們調整測試程式碼如下:

line 5:引入 axios ,這是我們要stub的對象。

line 14:預期要呼叫的url。

line 15–28:預期api的回傳資料。

line 30:將axios的 get 用我們的stub“替換”掉。並指示stub直接回傳我們想要的資料是 expectedData ,而不用呼叫遠端api。由於get是一個非同步方法,所以此處我們以 resolves 處理之。

line 33:執行被測試方法。

line 36–37:確認 get 方法被呼叫了一次,且目標的url( args[0] 即get方法的第一個參數)為我們預期的。

line 39:確認 getUserData 回傳的內容與我們預期的相符。

line 41:將被抽換掉的對象,再換回去。這步驟很重要,請確實執行,否則後續的測試可能會出現難以預期的錯誤。

以下是測試結果,可看出測試時間已恢復正常水準:

測試結果

如此,我們“切斷”了測試程式對外部環境的依賴,而能專注於“單元”測試。

關於單元測試,實在有太多東西可以講,本文只能算是驚鴻一瞥、牛刀小試,希望未來還有機會分享相關的主題。最後附上實際的範例: express 框架的測試,以供觀摩、學習。

--

--