原文:Testing Your Frontend Code: Part II (Unit Testing)html
By Gil Tayar前端
咱們在Part1裏已經說過,但與那測試就是測試單元的代碼,無論這些單元是函數、模塊仍是類。多數人認爲測試應該以單測爲主,但我不這麼認爲,若是你贊成也沒有問題。我會一遍一遍又一遍地在這一系列文章中強調,你怎測試都行,只要你寫了足夠多的測試,讓你對你的上線有信心就行。webpack
無論你寫多少單測,單測確實是最好寫也最好懂的測試,它們天生具備函數屬性。設定一個單元的輸入,執行,檢查輸出(輸入可能就是一個函數的參數,輸出就是返回值)。git
更重要的是你應該提醒本身寫代碼時要讓這些單元彼此隔離,不會相互依賴,這樣才能方便單測。github
理論已經夠多了,咱們看下咱們的計算器,源碼在這裏。這是個React應用,有兩個主要的組件,keypad
和display
。顯然他們是單元且對其餘單元無依賴。可是他們是React層面的單元,之後我會專門講如何測它們。web
若是你已經閱讀過代碼可能會疑問爲啥我不用JSX。緣由是我不想使用代碼轉譯。Node和現代的瀏覽器可以識別ES6,因此爲啥不讓你寫的代碼直接運行呢?是的,個人代碼不能再IE中運行,可是這只是個demo,因此沒問題。在一個真實的項目裏,我會添加代碼轉譯的。算法
譯註:最新的代碼倉庫已經用JSX重構並增長了babel轉譯,因此這段呵呵了。數據庫
總有些代碼負責處理當點擊了一個數字或者運算符後的運算,哪部分代碼完成這部分工做呢?按照如今時髦的作法,我也把個人組件分紅了展現型組件(keypad
和display
)和狀態型組件(calculator-app
)。後一個組件時惟一一個有狀態的組件,負責調用計算邏輯,並驅動display
展現點擊後應該顯示啥。npm
calculator
模塊負責計算的邏輯並不在組件中,它是一個單獨的模塊calculator
,並不依賴React。這樣的模塊是最適合單測的。對UI和IO沒有依賴的模塊最適合單測。你應該盡力讓你更多的業務邏輯以不依賴IO或者UI的形式寫成模塊。json
在前端裏,不依賴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或者其餘框架均可以。
全部的測試框架都是相似的——你把測試代碼寫成函數,測試框架負責執行它們。
當npm install
Moch後,咱們就能夠經過npm腳本運行了。固然,命令就是"Mocha"。看下package.json
,你會看到:
"scripts": {
...
"test": "mocha 'test/**/test-*.js' && eslint test lib",
...
},
複製代碼
運行npm test
,就會執行以test
打頭的文件夾裏的測試腳本。若是你clone了這個代碼倉庫,你要先npm install
。
(順便說下,把測試腳本放在根目錄下的test
文件夾中是測試的慣例,若是你想要讓別人認爲你寫測試很專業,那你也應該這麼作)
執行後,輸出長這個樣子。
若是有一個測試沒有經過,你會看到刺眼的紅色,而後就能夠立刻修改了。
看一下咱們的測試用例:
// 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
(稍後咱們會將啥事斷言庫)。引入一些咱們須要的函數describe
、it
。
而後引入咱們要測試的模塊——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環境中。若是你的代碼中沒有任何IO操做,這就意味着它並非只能在瀏覽器中運行。尤爲是,咱們的代碼使用了require
來組織代碼,既能被NodeJS識別,又能被Webpack打包(看下package.json
,你就會發現使用了webpack)。
"scripts": {
"build": "webpack && cp public/* dist",
...
}
複製代碼
順便提一下,咱們可使用karma來實如今瀏覽器中運行Mocha。可是我謹認爲若是能在Node中運行就在Node中運行(如今你其實很容易寫出在兩端都能運行的代碼),由於不管是運行仍是debug都更方便。不編譯代碼的話,運行起來會更快。
可是不在瀏覽器中跑測試,咱們就不能確認咱們的代碼能在瀏覽器中運行。兩個環境中的一些細微差異可能會致使一些問題。
上面說的問題就是E2E測試要管的事了——在真實的瀏覽器環境中測試咱們的代碼。下週咱們將如何寫E2E測試。
()