前端測試 (1) - Unit test, Mocha & Chai

工作以來一直需要對新開發的功能或 React 元件在上版前做測試,以保證所有組件與函式能如預期的運作。雖然感覺還沒完全摸透這方面的知識,但仍想紀錄一下自己到目前為止的理解以供日後參考。

Unit Test 單元測試

單元測試就是在程式專案 (project) 中對小單位的程式碼進行測試。聽起來好像很抽象,因為所謂的小單位程式碼測試項目是需要自行定義的。以現任公司的作法為例,測試的標的通常是函式、React 元件或整個頁面;而測試進行的項目,除了常見的輸入輸出正確性測試外,也可以測元件有沒有被正確渲染、特定事件有沒有被觸發、特定的變數值是否合乎預期等等。

好處

寫單元測試是費時的,除了要想元件在哪些特定情境可能會有問題,也需要花時間做能製造那些情況的測試資料。雖然麻煩,但卻能帶來以下好處:

  • 能自動化進行測試且產生量化的測試數據,並且能在短時間內完成
  • 避免在更動元件時改壞原本能正常運行的功能
  • 能逐步累積測試案例,盡可能確保元件能在所有情境下正常運作

什麼時候寫

其實也是取決於工程團隊的需求。以現任公司為例,我們通常在開發新的 React 元件或新增 function 到函式庫的時候,就會需要寫相對應的單元測式,例如:測試元件在 mount 的時候有沒有依照 props 或 state 的值正確呈現,或測試切換按鈕被點擊的時候,存在 state 裡相對應的數值有沒有改變等等。

工具

目前公司是用 Mocha, Chai 和 Sinon,安裝方式請參考官方網站,以下將逐一介紹他們的功能。

 

Mocha 測試框架

用來管理測試案例及定義如何進行測試,在每次啟動單元測試的時候都會被執行。

常用語法

  • describe(): 描述當前的測試標的,例如某某 function 或某某元件
  • it(): 包在 describe() 底下,描述要進行的測試案例,通常是元件的預期動作,例如:”should disappear when close button is clicked”
  • before(): 在跑第一個測試案例前進行的動作
  • after(): 在最後一個測試案例跑完後要執行的動作
  • beforeEach(): 在跑每個測試案例前都要進行的動作
  • afterEach(): 在完成每個測試案例後都要執行的動作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// name of a component/function to be tested
describe('<MyReactComponent/>', function() {
before(function() {
// runs once before the first test in this block
});

after(function() {
// runs once after the last test in this block
});

beforeEach(function() {
// runs before each test in this block
});

afterEach(function() {
// runs after each test in this block
});

// test cases
it('should have all valid values in required columns when clicking \
the submit button', function() {
// check all values in columns
// 如果元件行為與預期相符,無需回傳任何值
// 否則 throw error 給 Mocha 知道
// (注意:return false 對 Mocha 而言測試還是通過的!)
});
});

 

Chai 斷言庫

在先前的每個 it() 測試案例中,如果元件的行為不合預期,我們以 throw error 的方式告訴 Mocha 該元件沒有通過該案例。這樣雖然直觀好寫,但如果一個案例中有好幾個要檢查的地方就會開始變得複雜(要寫一堆 if-else cases 還有 throw errors),此外要產生的錯誤訊息也會變得難以整理與判斷。

斷言庫就是用來使這些事變容易的工具,除了驗證執行結果是否符合預期,其本身的寫法也能在這些錯誤訊息產生時自動予以分門別類,進而方便管理與判讀。這邊介紹的 Chai 就是其中一套斷言庫,可搭配 Mocha 使用。其他的還有像是:expect.js, should.js, unexpected 等等。

 

Chai 斷言庫提供三種斷言風格,分別是 assert, expect, should。個人覺得他們之間大同小異,選 1~2 種用即可。

1. assert

  • 通常寫法為:assert(expression, error_message)
  • 也可寫成:
    assert.<compare_function>(compare_target, correct_value, [error_message])
1
2
3
4
5
assert('foo' !== 'bar', 'foo is not bar');
assert.typeOf(foo, 'string');
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');

2. expect

  • expect()是一個函式,以測試標的為參數
  • 寫法貼近自然語言,可以串連多個斷言
  • 串連的斷言在預期結果不符時,可以自動生成錯誤訊息
  • 也可以另外自訂錯誤訊息
1
2
3
4
5
6
7
8
// expect(object)....
expect(foo).to.be.a('string');
expect(foo).to.have.lengthOf(3);

// AssertionError: expected 43 to equal 42.
expect(answer).to.equal(42);
// AssertionError: topic [answer]: expected 43 to equal 42.
expect(answer, 'Topic [answer]').to.equal(42);

3. should

  • expect 類似,可以串連多個斷言並自動生成錯誤訊息
  • 不一樣的是 should 是透過 Object.prototype 擴展而來,自動變成每個測試物件裡的屬性
  • 無法自訂錯誤訊息,且 IE 瀏覽器不支援這種寫法
  • 特定情況下需改變寫法,例如檢查物件是否存在
1
2
3
4
5
6
7
8
9
10
// object.should....
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
beverages.should.have.property('tea').with.lengthOf(3);

// Since objects could be null or undefined, the writing style
// should be modified as below to check for existence:
should.not.exist(err);
should.exist(doc);

 

小結

單元測試的測試標的、項目案例與斷言庫的選擇,很大程度取決於團隊的專案需求,並沒有一定的答案。這篇就先記錄到這邊,下一篇來聊聊 Sinon。

參考資料