用 Mocha 和 Chai 對 JavaScript 程式碼進行單元測試

劉健超-J.c發表於2016-11-15

你曾試過修改程式碼後,導致其它地方出現問題嗎?

我相信很多人都遇到過。因為這是幾乎不可避免的,特別在龐大的程式碼面前。由於程式碼間可能是環環相扣的,改變一處會影響另一處。

但如果這種情況不會發生呢?如果有一種方法能讓你知道改變後會出現的結果呢?這無疑是極好的。因為修改程式碼後無需擔心會破壞什麼東西,從而程式出現 bug 的概率更低,在 debug 上花費時間更少。

這就是單元測試的魅力。它能自動檢測程式碼中的任何問題。在修改程式碼後進行相應測試,若有問題,能立刻知道問題是什麼,問題在哪和正確的做法是什麼。這完全可以消除任何猜測!

在本文,我會讓你瞭解如何對 JavaScript 程式碼進行單元測試。而且,在本文出現的案例和技術可同時應用到基於瀏覽器的程式碼和 Node.js 的程式碼。

教程中的程式碼也可以在 GitHub repo 中得到。

什麼是單元測試

當你對程式碼庫進行測試時,可先取一段程式碼(通常是一個函式),然後在特定情況下,驗證其行為是否正確。而單元測試就是這方面的一種結構化和自動化的方法。當然,寫的測試越多,獲得的益處也更大。這也會讓你在開發時更加自信。

單元測試的核心思想是給函式特定的輸入值,測試其行為。也就是說,以特定的引數呼叫函式,然後檢查是否得到正確的結果。

在實際中,測試有時會更復雜。例如,如果你的函式含有一個 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

此時已經安裝了 mochachai 包(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

該測試執行器的幾個重要點:

  • 為了讓測試結果擁有漂亮的樣式,我們載入了 Mocha 的 CSS 檔案。
  • 建立了一個 ID 為 mochat 的 div 標籤。測試結果將放在該標籤內。
  • 載入 Mocha 和 Chai 指令碼檔案。由於這兩個檔案是通過 npm 安裝的,它們被放在 node_modules 目錄的子資料夾下。
  • 通過呼叫 mocha.setup,開啟 Mocha 的測試功能(testing helpers)。
  • 然後,載入需要的測試項和相應測試的檔案。儘管我們還沒在這放置任何程式碼。
  • 最後,呼叫了 mocha.run 執行相應測試。當然,要確保在資源和測試檔案載入完成後再呼叫該函式。

基本的測試骨架

現在我們可以執行測試了,下面就開始寫點測試相關的東西吧。

首先,建立 test/arrayTest.js。每個檔名都有其具體含義,顯然它是個測試檔案,並會測試 array 的基本功能。

每個測試案例檔案都會遵循以下基本模式。首先,有個 describe 塊:

describe 用於把單獨的測試聚合在一起。其第一個引數用於指示測試什麼。在本例中,由於我們打算測試 array 功能,我傳入一個 'Array' 字串。

然後,在 describe 內需有 it 塊:

it 用於建立實際的測試。其第一個引數是對該測試的描述,且該描述的語言應該是人類可讀的(而非程式語言)。如在本例中,“it should empty”能很好地描述了 array 的行為。實現該測試的具體程式碼則寫在 it 的第二個引數 function 內。

所有 Mocha 測試都以同樣的骨架編寫,而且它們遵循相同的基本模式。

  • 首先,使用 describe 表明我們測試什麼,如“描述 array 該如何執行”。
  • 然後,使用多個 it 函式建立獨立的測試,每個 it 應該描述一個特定的行為,如上述的案例 “it should start empty(array 執行前應為空)”

編寫測試程式碼

現在我們已經知道如何構造測試案例了,下面就開始更有趣的部分——實現測試。

由於我們的測試是 array 初始值應為空,即我們需要建立一個陣列並確保它為空。實現該測試是非常簡單的:

請注意首行程式碼,我們設定了 assert 變數。這樣就不用每次都輸入 chai.assert 了。

it 函式裡,我們建立了一個陣列並檢查其長度。儘管簡單,但很好地展示了測試是如何工作的。

首先,你有東西需要被測試——這叫 被測系統(System Under Test,SUT)。若有需要,則對被測系統進行相應操作。對於上述案例,由於檢查陣列初始值是否為空,我們沒做任何操作。

測試的最後步驟應該是驗證——對結果進行斷言(assertion)檢查。對於上述案例,我們對此使用 assert.equal。大多數斷言函式的引數順序是一致的:首先是“實際”值,然後是“期待”值。

實際值是測試程式碼的結果,因此,在該案例中是 arr.length

期待值是預想的結果。由於陣列的初始值應為空,因此,在該案例中的期待值是 0

雖然 Chai 提供了兩種不同的斷言(assertion)編寫方式,但現在為了保持簡單,我們使用了 assert。當你能熟練編寫測試時,你可能更想用 expect assertions ,因為它提供了更靈活的操作。

執行測試

為了執行該測試,我們需要將其新增到先前建立的測試執行器檔案內。

對於 Node.js,可跳過此步驟,然後使用命令 mocha 執行測試。你會在 terminal 裡看到測試結果。

向執行器新增該測試(針對瀏覽器端):

你一旦新增了指令碼,就可以載入測試執行器頁面了(若選擇在瀏覽器進行測試)。

測試結果

當你執行這些測試,其測試結果看起來和下圖類似:

Mocha test results - 1 test passing

注意:在 describeit 函式的描述語句都在頁面展示出來了——測試項(如:should start empty)都分組放在描述(如:Array)下。當然,也可以對 describe 塊再巢狀,以建立更深的子分組。

下面看看測試失敗會顯示什麼。

將測試的該行程式碼進行修改:

將 0 改為 1。這無疑會導致測試失敗,因為陣列長度不再匹配期待值。

如果你再次執行測試,那麼在測試結果中,執行錯誤的描述將以紅色顯示。

Mocha test error - one test failing

測試的一項好處是能幫助你更快地找到 bug,儘管錯誤資訊在這並不是非常詳細。但是我們可以解決這個問題。

大多數斷言函式都帶有一個可選的 message 引數。該資訊引數會在斷言失敗時顯示。因此我們可以利用該引數,讓錯誤資訊更容易理解。

我們能像下面那樣向斷言新增 message 引數:

如果你再次執行測試,那麼自定義的資訊會取代預設的資訊而顯示出來。

OK,讓我們將 1 改回 0,確保測試通過。

綜合案例

到目前為止,案例都是相當簡單的。那麼下面就讓我們將學到的知識付諸實踐,看看如何測試一段實際當中所用到的程式碼。

下面是一個將 CSS 類名新增到元素的函式。我們將該函式放進新檔案 js/className.js

當元素的 className 屬性不含有新類名時,才向元素新增新類名——畢竟誰想看到 <div class="hello hello hello hello">

在最好的情況下,我們要在編寫程式碼前先為該函式編寫測試。但 測試驅動開發(test-driven development) 是一個複雜的主題,因此我們現在僅專注於編寫測試。

開始前,讓我們重溫單元測試的基本思想:賦予函式特定的輸入值,然後驗證函式的行為是否符合預期。所以,該函式的輸入值和行為是什麼呢?

給定一個元素和一個類名:

  • 若元素的 className 屬性未含有該類名,則應新增。
  • 若元素的 className 屬性已含有該類名,則不應新增。

將這兩種情況轉化為兩個測試。在 test 目錄下,建立新檔案 classNameTest.js 並新增以下內容:

我們也可以將措詞稍微地改成“it should do X”,雖然可讀性更強一點,但本質上仍然與我們上述語句的可讀性一致。根據原來的措詞聯想到相應的測試也不難。

等等,測試函式跑去哪了?當我們省略 it 的第二個引數,Mocha 會在測試結果中標記這些測試為待測試項。這讓設定多個測試變得更方便——就像一個備忘錄,列著打算編寫的測試。

接著實現第一個測試。

在該測試中,我們建立了 element 變數,並將其與字串 test-class(作為元素的新類名) 作為引數傳入 addClass 函式。然後,使用斷言檢查該類名是否已包含在值(element.className)裡。

再一次,我們從初始的想法出發——給定一個元素和一個類名,將類名新增到 class 列表,然後以簡單的方式將其轉化為程式碼。

儘管該函式(addClass)是針對 DOM 元素的,但我們在此使用了一個簡單 JS 物件(plain JS object,根據 jQuery 官方定義:含有零個或多個鍵值對的物件)。是的,有時我們可以利用 JavaScript 的動態特性,以上述方式簡化測試。如果不這樣做,我們就要建立一個實際的元素,這無疑會使測試程式碼變複雜。當然,這還有另一個好處,由於沒使用 DOM,該測試也能在 Node.js 執行。

在瀏覽器執行測試

為了在瀏覽器執行測試,你需要在執行器新增 className.jsclassNameTest.js

正如下面 CodePen 中所顯示的:一個測試通過,而另一個顯示待測試。注意:為了讓程式碼執行在 CodePen 環境下,程式碼需稍作調整。

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

接著,實現第二個測試…

經常執行測試是一種好習慣。因此,讓我們現在執行測試看看會發生什麼。

不出所料,兩者均通過。

下面是在 CodePen 中實現第二個測試的例子。

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

但事情沒那麼簡單!該函式的第三種情況我們並沒有考慮到,這也是該函式的一個非常嚴重的 Bug。雖然該函式只有三行程式碼,但你注意到了嗎?

下面為第三種情況編寫多一個案例,讓這個 Bug 暴露出來。

你可在下面的 CodePen 中看到,這次測試失敗了。導致該問題的原因很簡單:元素上的 CSS 類名應以空格隔開。然而,現在實現的 addClass 並未加空格!

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

修復該函式,讓測試通過。

修復後,最終在 CodePen 測試通過。

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

在 Node 中執行測試

在 Node 中,只有同一檔案中的內容是可見的。由於 className.jsclassNameTest.js 在不同檔案下,我們需要一種方式將一個檔案匯出到另一個檔案內。而標準的方式是通過 module.exports。如果你需要複習相關知識,你可以看看 Understanding module.exports and exports in Node.js

程式碼本質不變,只是結構稍微不同:

正如你所看到的,測試通過。

Mocha terminal output - 4 tests passing

下一步呢?

正如你所看到的,測試不復雜也不難。與編寫 JavaScript 應用的其它方面一樣,有一些重複的基本模式。一旦你熟悉了這些,你可以一次又一次的使用它們。

但這些只是單元測試的皮毛,還有很多相關知識需要學習。

  • 測試更復雜的系統
  • 如何處理Ajax、資料庫和其它“外部”的東西。
  • 測試驅動開發

如果你想繼續學習更多相關知識,可看看我編寫的 免費的 JavaScript 單元測試快速入門系列。如果你覺得本文有用,你更應該點選 這裡 看看。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

用 Mocha 和 Chai 對 JavaScript 程式碼進行單元測試 用 Mocha 和 Chai 對 JavaScript 程式碼進行單元測試

相關文章