前端測試:Part II (單元測試)

妖僧風月發表於2018-06-27

原文:Testing Your Frontend Code: Part II (Unit Testing)

By Gil Tayar

單元測試

我們在Part1裡已經說過,但與那測試就是測試單元的程式碼,不管這些單元是函式、模組還是類。多數人認為測試應該以單測為主,但我不這麼認為,如果你同意也沒有問題。我會一遍一遍又一遍地在這一系列文章中強調,你怎測試都行,只要你寫了足夠多的測試,讓你對你的上線有信心就行。

不管你寫多少單測,單測確實是最好寫也最好懂的測試,它們天生具有函式屬性。設定一個單元的輸入,執行,檢查輸出(輸入可能就是一個函式的引數,輸出就是返回值)。

更重要的是你應該提醒自己寫程式碼時要讓這些單元彼此隔離,不會相互依賴,這樣才能方便單測。

計算器應用中的單元

理論已經夠多了,我們看下我們的計算器,原始碼在這裡。這是個React應用,有兩個主要的元件,keypaddisplay。顯然他們是單元且對其他單元無依賴。但是他們是React層面的單元,以後我會專門講如何測它們。

如果你已經閱讀過程式碼可能會疑問為啥我不用JSX。原因是我不想使用程式碼轉譯。Node和現代的瀏覽器能夠識別ES6,所以為啥不讓你寫的程式碼直接執行呢?是的,我的程式碼不能再IE中執行,但是這只是個demo,所以沒問題。在一個真實的專案裡,我會新增程式碼轉譯的。

譯註:最新的程式碼倉庫已經用JSX重構並增加了babel轉譯,所以這段呵呵了。

總有些程式碼負責處理當點選了一個數字或者運算子後的運算,哪部分程式碼完成這部分工作呢?按照現在時髦的做法,我也把我的元件分成了展示型元件(keypaddisplay)和狀態型元件(calculator-app)。後一個元件時唯一一個有狀態的元件,負責呼叫計算邏輯,並驅動display展示點選後應該顯示啥。

calculator模組

負責計算的邏輯並不在元件中,它是一個單獨的模組calculator,並不依賴React。這樣的模組是最適合單測的。對UI和IO沒有依賴的模組最適合單測。你應該盡力讓你更多的業務邏輯以不依賴IO或者UI的形式寫成模組。

在前端裡,不依賴IO是啥意思呢?不訪問檔案、資料庫… ?不,前端裡本來就沒有這些。但是還會有Ajax,local storage,DOM 訪問,瀏覽器API訪問,對我而言,這些都是IO。

我是如何把計算器的邏輯和元件分開的?在這裡,其實很簡單,這些邏輯都是演算法類的,我把它們放到了calculator模組中。

這個模組非常簡單,輸入是計算器的當前狀態(一個物件)和一個字元(一個數字或者運算子),返回值是計算器的新狀態。如果你用過Redux,這個邏輯跟Redux的Reducer差不多。但是如何獲取最原始的狀態呢?簡單,這個模組同樣Export了initialState,你可以用它去初始化計算器。計算器的狀態並非不透明的,它包含可一個元件display,用來將計算器的內在狀態顯示出來。

如果你沒有耐心去仔細閱讀程式碼,我們這裡只看下開頭就行,這部分最重要,演算法是怎麼實現的其實無關緊要。

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
  }
}

//....
複製程式碼

怎麼測試呢?我們通常會用一個測試框架。當下最流行的測試框架是Mocha,我們就用它來測試。當然用Jest,Jasmine,Tape或者其他框架都可以。

用Mocha進行單測

所有的測試框架都是類似的——你把測試程式碼寫成函式,測試框架負責執行它們。

npm installMoch後,我們就可以通過npm指令碼執行了。當然,命令就是"Mocha"。看下package.json,你會看到:

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

執行npm test,就會執行以test打頭的資料夾裡的測試指令碼。如果你clone了這個程式碼倉庫,你要先npm install

(順便說下,把測試指令碼放在根目錄下的test資料夾中是測試的慣例,如果你想要讓別人認為你寫測試很專業,那你也應該這麼做)

執行後,輸出長這個樣子。

前端測試:Part II (單元測試)

如果有一個測試沒有通過,你會看到刺眼的紅色,然後就可以馬上修改了。

看一下我們的測試用例:

// test-calculator.js
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(稍後我們會將啥事斷言庫)。引入一些我們需要的函式describeit

然後引入我們要測試的模組——calculator

然後就是使用it函式定義的測試用例了,如下:

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

it函式接受一個字串引數,用來描述測試用例,另一個引數是一個函式,就是測試本身了。但是it是不能“裸奔”的,需要被包裹在describe函式定義的測試組中。

在測試邏輯中寫啥呢?其實啥都可以。在這裡我們就是判斷了下初始展示的值是不是等於0. 如果不用expect,我們可以這麼寫:

if (calculator.initialState.display !== '0')
  throw 'failed'
複製程式碼

Mocha中如果一個測試不通過,就會丟擲一個異常,就是這麼簡單。但是使用expect讓我們可以使用他的一些特性來方便地檢查陣列、物件的值。

這就是單元測試的主旨了——執行一個或者一組函式(如果你是物件導向測試,則通常例項化一個物件,然後呼叫它的方法),檢查返回的結果是否等於預期結果。

編寫可測試的程式碼

單測裡最複雜的不是測試本身,而是分離程式碼,從而讓它們儘可能地變得可測。**可單測的程式碼就是,對其他模組和IO沒有依賴的程式碼。**這並不簡單,因為我們通常習慣於講業務邏輯和IO,UI耦合起來。但是這個目標仍然是可以達到的,有很多技術。例如,如果你有一段驗證表單的程式碼,把它們分離出來變成一個一個的驗證函式,然後對它們進行測試。

測試程式碼執行在Node環境下?

注意到很重要的一點——單測是執行在Node環境下的。即使計算器應用的程式碼是跑在瀏覽器中,我們仍然使用Node去跑我們的測試,包括要上線的程式碼。

這怎麼能行呢?這是因為我們的程式碼是同構的。這意味著它們能同時執行在瀏覽器和Node環境中。如果你的程式碼中沒有任何IO操作,這就意味著它並不是只能在瀏覽器中執行。尤其是,我們的程式碼使用了require來組織程式碼,既能被NodeJS識別,又能被Webpack打包(看下package.json,你就會發現使用了webpack)。

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

在瀏覽器環境下單測

順便提一下,我們可以使用karma來實現在瀏覽器中執行Mocha。但是我謹認為如果能在Node中執行就在Node中執行(現在你其實很容易寫出在兩端都能執行的程式碼),因為無論是執行還是debug都更方便。不編譯程式碼的話,執行起來會更快。

但是不在瀏覽器中跑測試,我們就不能確認我們的程式碼能在瀏覽器中執行。兩個環境中的一些細微差別可能會導致一些問題。

下週

上面說的問題就是E2E測試要管的事了——在真實的瀏覽器環境中測試我們的程式碼。下週我們將如何寫E2E測試。

()

相關文章