上一篇文章《測試你的前端程式碼 – part1(介紹)》中,我介紹了關於前端測試的基本知識,從本文開始將具體介紹測試技術。
單元測試
上一節有討論過,單元測試就是以程式碼單元為單位進行測試,程式碼單元可以是一個函式,一個模組,或者一個類。很多人認為大多數測試都應該叫單元測試,其實我的觀點還是那句話,無所謂怎麼叫,名字叫什麼都行。只要你做了足夠多的測試,能夠保證你部署到線上的生產程式碼沒有問題就可以了。
單元測試是最容易理解、也最容易實現的測試方式。給單元測試一個輸入,讓它自動執行,將輸出結果和預期結果做對比看其是否正確(輸入可以是一個函式引數,輸出就是函式的返回值)。
在寫單元測試的時候,儘量將你的單元測試獨立出來,不要幾個單元互相引用。養成這樣良好的測試習慣。
測試 Calculator 應用
第一節中提到過,為了這系列博文,我寫了一個計算器應用,後面都會拿它進行測試。理論就講到這裡,一起來看一下 Calculator 應用吧,原始碼在這裡。主要有兩個元件: keypad 和 display ,它們自身都是 React 單元,也都沒有引用其他單元,後面會介紹如何對它們進行測試。
(如果你已經看了程式碼可能已經發現了我沒有使用 JSX。因為我不想進行轉譯。現在 Node 和所有流行的瀏覽器都已經完全支援 ES6 了,那麼作為一個例子來講,讓它直接執行會更好一些。雖然它不能執行在 IE 上,不過也沒關係,如果是一個真實的線上專案,我會進行轉譯的。)
還有一個問題是按鍵和展示的邏輯問題,必須要有程式碼來控制當點選按鍵的時候發生什麼。這裡的按鍵包括數字鍵(如“1”,“5”)和操作鍵(如“+”,“=”)。按通常的做法,我把元件設計成了展示型元件(鍵盤)和容器型元件。容器型元件在我的 App 中是唯一包含 state 的元件,它要考慮當發生按鍵行為的時候 App 內在邏輯的問題。
“calculator”模組
處理邏輯問題的程式碼是一個單獨的模組——calculator。這個模組對於單元測試是很完美的例子。因為它沒有對 I/O 和 UI 的依賴。你也應該儘量使你的應用邏輯上保持獨立——模組不依賴於 I/O 和 UI。
對於 Web 應用來講,I/O 是什麼?沒有檔案和資料庫的操作?其實不僅僅是這樣,還有 Ajax 呼叫,本地儲存,DOM 操作等,對我而言,任何和瀏覽器 API 有關的都是 I/O 操作。
我是怎麼把計算邏輯從 React 元件中分離出來的呢?其實很簡單,其內在邏輯是計算,我把他封裝到一個模組中就可以了。
這個模組的實現也很容易——它接收一個計算器 state(一個物件)和一個字元(數字或者操作符),返回一個新的計算器 state。如果你用過 Redux,它很像 Redux 的 reducer 模式(如果你沒用過 Redux 也沒關係)。但是如果一直由上一個 state 獲取下一個 state,怎麼能回到初始狀態呢?這裡還有一個模組叫做 initialState,通過它可以初始化計算器。計算器的 state 並不是完全黑盒的,它包含了一個欄位叫做 display,可以把你想要展示的 state 顯示在計算器應用上。
如果你沒有耐心看原始碼的話,我們一起來看下這裡面最重要的部分,應用中演算法的細節其實不重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module.exports.initialState = { display: '0', initial: true } module.exports.nextState = (calculatorState, character) => { if (isDigit(character)) { return addDigit(calculatorState, character) } else if (isOperator(character)) { return addOperator(calculatorState, character) } else if (isEqualSign(character)) { return compute(calculatorState) } else { return calculatorState } } //.... |
再次強調這裡的實現細節並不重要,重要的是模組的設計,它暴露出來的函式非常簡單——給一個 state,得到下一個 state。
這就是我們在 test-calculator 中所做的事情。那麼接下來怎麼進行測試呢?使用測試框架,目前比較流行的框架是 Mocha ,我們就用它。不過像 Jest,Jasmine,Tape等框架也都行,隨意使用你喜歡的測試框架。
用 Mocha 進行單元測試
所有的測試框架都類似,寫測試程式碼呼叫被測函式,通過測試框架執行他們,其中執行它們的程式碼通常叫做“runner”。
Mocha runner 叫做 “mocha”,如果你看測試指令碼的 package.json,可以看到:
1 2 3 4 5 |
"scripts": { ... "test": "mocha 'test/**/test-*.js' && eslint test lib", ... }, |
它會執行 test 資料夾中所有以 test- 開頭的檔案,你可以複製我的 repo,npm install 後,執行 npm test 自己試試。
(順便提一句,把所有測試都放在測試目錄,並且測試目錄放在 package 的根目錄是一個公認的 npm package 約定,如果你不想讓人覺得你不專業的話,最好還是遵守這一約定。)
執行它,會得到如下輸出:
這裡有 14 個測試通過的提示資訊,如果沒通過,就會有紅色提示出現。
我們看下面程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const {describe, it} = require('mocha') const {expect} = require('chai') const calculator = require('../../lib/calculator') describe('calculator', function () { const stream = (characters, calculatorState = calculator.initialState) => !characters ? calculatorState : stream(characters.slice(1), calculator.nextState(calculatorState, characters[0])) it('should show initial display correctly', () => { expect(calculator.initialState.display).to.equal('0') }) it('should replace 0 in initialState', () => { expect(stream('4').display).to.equal('4') }) //... |
首先引入 mocha 和斷言常量 expect,這裡只引入我們需要的函式:describe,it 和 expect。接下來引入我們要測試的模組 calculator。
準備開始測試,用 it 函式來表達:
1 2 3 |
it('should show initial display correctly', () => { expect(calculator.initialState.display).to.equal('0') }) |
it 函式接收一個字串(用來表示測試結果)和一個函式(待測函式)。it 測試不能單獨執行,它們必須組成一個測試組。所以如程式碼中所示,用 describe 函式定義測試組,裡面包含了若干個 it 函式。
測試函式中寫什麼呢?可以寫任何想寫的東西,在這個例子中我們測試了初始狀態所顯示的是不是 0。如果我們自己來實現怎麼實現呢,比如可以像如下程式碼:
1 2 |
if (calculator.initialState.display !== '0') throw 'failed' |
對於這個問題,上面程式碼也是可以測出來的。但是 expect 包含了很多特性可以使測試變得更簡單,比如可以測試陣列或者物件是否和一個給定的值相等。這就是單元測試的要點,即執行一個函式,或一組函式,檢查其 執行結果 是否和 預期結果 一致。
編寫單元可測的程式碼
上面的很簡單對吧!其實對於單元測試來講,難的並不是單元測試本身,而是分離程式碼的藝術,把程式碼儘量分離成單元可測的模組。單元可測的程式碼一般都是不依賴於其他模組、不依賴於 I/O 的程式碼。這是比較困難的,大多數人都傾向於把邏輯程式碼、I/O 程式碼和 UI 程式碼寫到一起。困難是困難,但不是說做不到,有很多技巧可以使用,比如你的程式碼中有一些驗證欄位,那麼你就可以把驗證程式碼組織到一起形成函式,再對這個驗證函式進行測試。
測試程式碼是執行在 NodeJS 下的!?
注意一個重要的事情——單元測試是在 NodeJS 下執行的!而計算器應用是執行在瀏覽器端的,上面的生產程式碼都是在 NodeJS 下進行測試的,這也可以嗎?
當然可以。因為我們的程式碼是同構的,它可以執行在瀏覽器端和 NodeJS 上。如果你的程式碼沒有使用任何 I/O,就是說沒有對瀏覽器做任何的特化處理,那麼它就沒有理由不能執行在 NodeJS 上。另外,如果你使用了 require,它既可以被本地的 NodeJS 識別,也可以被像 Webpack 一樣的打包器識別。你看程式碼中的 package.json,就可以看到我們就是使用了 Webpack,用 require 進行程式碼打包:
1 2 3 4 |
"scripts": { "build": "webpack && cp public/* dist", ... } |
程式碼中使用 require 來引入 React 或者其他模組,這不論是在 NodeJS 中還是瀏覽器中都是通用的。
在瀏覽器中執行單元測試
我們還可以使用另一個測試框架,Karma 。使用它可以在瀏覽器中執行 Mocha 程式碼,但是這裡表達一下我的淺見:單元測試能在 Node 下執行就在 Node 下執行,因為很容易執行和 debug(當然現在在瀏覽器中執行也很方便)。並且如果程式碼不需要轉譯的話,執行的也非常快。
但是我們的程式碼沒有在瀏覽器中測試確實是個問題,因為我們並不真正地知道程式碼在瀏覽器中執行會是什麼樣子。瀏覽器中的 JS 執行環境和 NodeJS 環境可能會有微妙的差別。
總結
本文中主要介紹了什麼:
- 介紹瞭如何使用 Mocha (和 Chai)建立單元測試;
- 介紹了單元測試就是以程式碼單元為單位進行測試,這個程式碼單元是獨立於其他模組的。
- 介紹了設計模組時應該獨立於其他模組。如果一定要有依賴,那麼可以 mock 一個其他模組對本模組進行單元測試,或者進行整合測試。
- 介紹了我們測試的程式碼單元應該是同構的,這樣就可以在 NodeJS 環境下進行測試了。
- 介紹瞭如何寫同構程式碼——沒有 I/O操作、使用 require 引入模組、使用 Webpack 來打包模組以使其符合瀏覽器執行環境。