測試你的前端程式碼 - part2(單元測試)

鬍子大哈發表於2017-03-24

本文作者:Gil Tayar

編譯:鬍子大哈

翻譯原文:blog.huziketang.com/blog/posts/…

英文連線:Testing Your Frontend Code: Part II (Unit Testing)

轉載請註明出處,保留原文連結以及作者資訊

上一篇文章《測試你的前端程式碼 - 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 顯示在計算器應用上。

如果你沒有耐心看原始碼的話,我們一起來看下這裡面最重要的部分,應用中演算法的細節其實不重要。

    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,可以看到:

    "scripts": {
    ...
        "test": "mocha 'test/**/test-*.js' && eslint test lib",
    ...
    },複製程式碼

它會執行 test 資料夾中所有以 test- 開頭的檔案,你可以複製我的 repo,npm install 後,執行 npm test 自己試試。

(順便提一句,把所有測試都放在測試目錄,並且測試目錄放在 package 的根目錄是一個公認的 npm package 約定,如果你不想讓人覺得你不專業的話,最好還是遵守這一約定。)

執行它,會得到如下輸出:

測試你的前端程式碼 - part2(單元測試)

這裡有 14 個測試通過的提示資訊,如果沒通過,就會有紅色提示出現。

我們看下面程式碼:

    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,這裡只引入我們需要的函式:describeitexpect。接下來引入我們要測試的模組 calculator

準備開始測試,用 it 函式來表達:

    it('should show initial display correctly', () => {
        expect(calculator.initialState.display).to.equal('0')
    })複製程式碼

it 函式接收一個字串(用來表示測試結果)和一個函式(待測函式)。it 測試不能單獨執行,它們必須組成一個測試組。所以如程式碼中所示,用 describe 函式定義測試組,裡面包含了若干個 it 函式。

測試函式中寫什麼呢?可以寫任何想寫的東西,在這個例子中我們測試了初始狀態所顯示的是不是 0。如果我們自己來實現怎麼實現呢,比如可以像如下程式碼:

    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 進行程式碼打包:

    "scripts": {
       "build": "webpack && cp public/* dist",
       ...
    }複製程式碼

程式碼中使用 require 來引入 React 或者其他模組,這不論是在 NodeJS 中還是瀏覽器中都是通用的。

在瀏覽器中執行單元測試

我們還可以使用另一個測試框架,Karma 。使用它可以在瀏覽器中執行 Mocha 程式碼,但是這裡表達一下我的淺見:單元測試能在 Node 下執行就在 Node 下執行,因為很容易執行和 debug(當然現在在瀏覽器中執行也很方便)。並且如果程式碼不需要轉譯的話,執行的也非常快。

但是我們的程式碼沒有在瀏覽器中測試確實是個問題,因為我們並不真正地知道程式碼在瀏覽器中執行會是什麼樣子。瀏覽器中的 JS 執行環境和 NodeJS 環境可能會有微妙的差別。

總結

本文中主要介紹了什麼:

  • 介紹瞭如何使用 Mocha (和 Chai)建立單元測試;
  • 介紹了單元測試就是以程式碼單元為單位進行測試,這個程式碼單元是獨立於其他模組的。
  • 介紹了設計模組時應該獨立於其他模組。如果一定要有依賴,那麼可以 mock 一個其他模組對本模組進行單元測試,或者進行整合測試。
  • 介紹了我們測試的程式碼單元應該是同構的,這樣就可以在 NodeJS 環境下進行測試了。
  • 介紹瞭如何寫同構程式碼——沒有 I/O操作、使用 require 引入模組、使用 Webpack 來打包模組以使其符合瀏覽器執行環境。

下文簡介

下篇文章我們介紹端到端測試,把我們的程式碼在真實環境(瀏覽器)中測試。請看下一篇文章《測試你的前端程式碼 - part3(端到端測試)》


我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點

相關文章