首先宣告一點,長期以來,前端開發的單元測試並不是在前端的開發過程中所必須的,也不是每個前端開發工程師所注意和重視的,甚至擴大到軟體開發過程中單元測試這一環也不是在章程上有書面規定所要求的。但是隨著每個工程的複雜化、程式碼的高複用性要求和前端程式碼模組之間的高內聚低耦合的需求,前端工程中的單元測試流程就顯得很有其必要。
1.前端單元測試是什麼
首先我們要明確測試是什麼:
為檢測特定的目標是否符合標準而採用專用的工具或者方法進行驗證,並最終得出特定的結果。
對於前端開發過程來說,這裡的特定目標就是指我們寫的程式碼,而工具就是我們需要用到的測試框架(庫)、測試用例等。檢測處的結果就是展示測試是否通過或者給出測試報告,這樣才能方便問題的排查和後期的修正。
基於測試“是什麼”的說法,為便於剛從事前端開發的同行的進階理解,那我們就列出單元測試它“不是什麼”:
需要訪問資料庫的測試不是單元測試
需要訪問網路的測試不是單元測試
需要訪問檔案系統的測試不是單元測試
--- 修改程式碼的藝術
對於單元測試“不是什麼”的引用解釋,至此點到為止。鑑於篇幅限制,對於引用內容,我想前端開發的同行們看到後會初步有一個屬於自己的理解。
2.單元測試的意義以及為什麼需要單元測試
2.1 單元測試的意義
對於現在的前端工程,一個標準完整的專案,測試是非常有必要的。很多時候我們只是完成了專案而忽略了專案測試的部分,測試的意義主要在於下面幾點:
- TDD(測試驅動開發) 被證明是有效的軟體編寫原則,它能覆蓋更多的功能介面。
- 快速反饋你的功能輸出,驗證你的想法。
- 保證程式碼重構的安全性,沒有一成不變的程式碼,測試用例能給你多變的程式碼結構一個定心丸。
- 易於測試的程式碼,說明是一個好的設計。做單元測試之前,肯定要例項化一個東西,假如這個東西有很多依賴的話,這個測試構7. 造過程將會非常耗時,會影響你的測試效率,怎麼辦呢?要依賴分離,一個類儘量保證功能單一,比如檢視與功能分離,這樣的話,你的程式碼也便於維護和理解。
2.2 為什麼需要單元測試
- 首先是一個前端單元測試的根本性原由:JavaScript 是動態語言,缺少型別檢查,編譯期間無法定位到錯誤; JavaScript 宿主的相容性問題。比如 DOM 操作在不同瀏覽器上的表現。
- 正確性:測試可以驗證程式碼的正確性,在上線前做到心裡有底。
- 自動化:當然手工也可以測試,通過console可以列印出內部資訊,但是這是一次性的事情,下次測試還需要從頭來過,效率不能得到保證。通過編寫測試用例,可以做到一次編寫,多次執行。
- 解釋性:測試用例用於測試介面、模組的重要性,那麼在測試用例中就會涉及如何使用這些API。其他開發人員如果要使用這些API,那閱讀測試用例是一種很好地途徑,有時比文件說明更清晰。
- 驅動開發,指導設計:程式碼被測試的前提是程式碼本身的可測試性,那麼要保證程式碼的可測試性,就需要在開發中注意API的設計,TDD將測試前移就是起到這麼一個作用。
- 保證重構:網際網路行業產品迭代速度很快,迭代後必然存在程式碼重構的過程,那怎麼才能保證重構後程式碼的質量呢?有測試用例做後盾,就可以大膽的進行重構。
3.如何寫單元測試用例
3.1 原則
- 測試程式碼時,只考慮測試,不考慮內部實現
- 資料儘量模擬現實,越靠近現實越好
- 充分考慮資料的邊界條件
- 對重點、複雜、核心程式碼,重點測試
- 利用AOP(beforeEach、afterEach),減少測試程式碼數量,避免無用功能
- 測試、功能開發相結合,有利於設計和程式碼重構
3.2 兩個常用的單元測試方法論
在單元測試中,常用的方法論有兩個:TDD(測試驅動開發)&BDD(行為驅動開發)
對於之前沒聽說過前端測試這兩個模式的同行可以在此瞭解一下,篇幅限制此處不再敖述。
3.3 相信你看完之後也有一個自己對TDD和BDD的個人觀點,在此我先談談我對TDD和BDD的 理解:
TDD(Test-driven development):
其基本思路是通過測試來推動整個開發的進行。
單元測試的首要目的不是為了能夠編寫出大覆蓋率的全部通過的測試程式碼,而是需要從使用者(呼叫者)的角度出發,嘗試函式邏輯的各種可能性,進而輔助性增強程式碼質量
測試是手段而不是目的。測試的主要目的不是證明程式碼正確,而是幫助發現錯誤,包括低階的錯誤
測試要快。快速執行、快速編寫
測試程式碼保持簡潔
不會忽略失敗的測試。一旦團隊開始接受1個測試的構建失敗,那麼他們漸漸地適應2、3、4或者更多的失敗。在這種情況下,測試集就不再起作用
需要注意的是:
一定不能誤解了TDD的核心目的!
測試不是為了覆蓋率和正確率
而是作為例項,告訴開發人員要編寫什麼程式碼
紅燈(程式碼還不完善,測試掛)-> 綠燈(編寫程式碼,測試通過)-> 重構(優化程式碼並保證測試通過)
TDD的過程是:
需求分析,思考實現。考慮如何“使用”產品程式碼,是一個例項方法還是一個類方法,是從建構函式傳參還是從方法呼叫傳參,方法的命名,返回值等。這時其實就是在做設計,而且設計以程式碼來體現。此時測試為紅
實現程式碼讓測試為”綠燈“
重構,然後重複測試
最終符合所有要求即:
每個概念都被清晰的表達
程式碼中無自我重複
沒有多餘的東西
通過測試
BDD(Behavior-driven development):
行為驅動開發(BDD),重點是通過與利益相關者(簡單說就是客戶)的討論,取得對預期的軟體行為的認識,其重點在於溝通
BDD過程是:
從業務的角度定義具體的,以及可衡量的目標
找到一種可以達到設定目標的、對業務最重要的那些功能的方法
然後像故事一樣描述出一個個具體可執行的行為。其描述方法基於一些通用詞彙,這些詞彙具有準確無誤的表達能力和一致的含義。例如,
expect
,should
,assert
尋找合適語言及方法,對行為進行實現
測試人員檢驗產品執行結果是否符合預期行為。最大程度的交付出符合使用者期望的產品,避免表達不一致帶來的問題
4. Mocha/Karma+Travis.CI的前端測試工作流
以上內容從什麼是單元測試談到單元測試的方法論。那麼怎樣用常用框架進行單元測試?單元測試的工具環境是什麼?單元測試的實際示例是怎樣的?
首先應該簡單介紹一下Mocha、Karma和Travis.CI
Mocha:mocha 是一個功能豐富的前端測試框架。所謂"測試框架",就是執行測試的工具。通過它,可以為JavaScript應用新增測試,從而保證程式碼的質量。mocha 既可以基於 Node.js 環境執行 也可以在瀏覽器環境執行。欲瞭解更多可去官方網站進行學習。其官方介紹為:
Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub.
Karma:一個基於Node.js的JavaScript測試執行過程管理工具(Test Runner)。該工具可用於測試所有主流Web瀏覽器,也可整合到CI(Continuous integration)工具,也可和其他程式碼編輯器一起使用。這個測試工具的一個強大特性就是,它可以監控檔案的變化,然後自行執行,通過console.log顯示測試結果。Karma的一個強大特性就是,它可以監控一套檔案的變換,並立即開始測試已儲存的檔案,使用者無需離開文字編輯器。測試結果通常顯示在命令列中,而非程式碼編輯器。這也就讓 Karma 基本可以和任何 JS 編輯器一起使用。
Travis.CI: 提供的是持續整合服務(Continuous Integration,簡稱 CI)。它繫結 Github 上面的專案,只要有新的程式碼,就會自動抓取。然後,提供一個執行環境,執行測試,完成構建,還能部署到伺服器。
持續整合指的是隻要程式碼有變更,就自動執行構建和測試,反饋執行結果。確保符合預期以後,再將新程式碼"整合"到主幹。
持續整合的好處在於,每次程式碼的小幅變更,就能看到執行結果,從而不斷累積小的變更,而不是在開發週期結束時,一下子合併一大塊程式碼。
對於Travis.CI,建議移步到阮大大和廖大大的個人網站上學習,兩位老師講的要比我在這兒寫的更清晰。
斷言庫
基本工具框架介紹完畢後,相信稍微瞭解點測試的同行都知道,做單元測試是需要寫測試指令碼的,那麼測試指令碼就需要用到斷言庫。”斷言“,個人理解即為”用彼程式碼斷定測試此程式碼的正確性,檢驗並暴露此程式碼的錯誤。“那麼對於前端單元測試來說,有以下常用斷言庫:
看一段程式碼示例:
expect(add(1, 1)).to.be.equal(2);
這是一句斷言程式碼。
引入斷言庫程式碼示例:
var expect = require('chai').expect;
斷言庫有很多種,Mocha 並不限制使用哪一種,它允許你使用你想要的任何斷言庫。上面程式碼引入的斷言庫是 chai,並且指定使用它的 expect 斷言風格。下面這些常見的斷言庫:
此處主要介紹一下node assert中常用的API
- assert(value[, message])
- assert.ok(value[, message])
- assert.equal(actual, expect[, message])
- assert.notEqual(actual, expected[, message])
- assert.strictEqual(actual, expect[, message])
- assert.notStrictEqual(actial, expected[, message])
- assert.deepEqual(actual, expect[, message])
- assert.notDeepEqual(actual, expected[, message])
- assert.deepStrictEqual(actual, expect[, message])
- assert.notDeepStrictEqual(actual, expected[, message])
- assert.throws(block[, error][, message])
- assert.doesNotThrow(block[, error][, message])
assert(value[, message])
斷言 value 的值是否為true,這裡的等於判斷使用的是 == 而不是 ===。message 是斷言描述,為可選引數。
const assert = require('assert');
assert(true);
複製程式碼
assert.ok(value[, message])
使用方法同 assert(value[, message])
。
assert.equal(actual, expect[, message])
預期 actual 與 expect值相等。equal用於比較的 actual 和 expect 是基礎型別(string, number, boolearn, null, undefined)的資料。其中的比較使用的是 == 而不是 ===。
it('assert.equal', () => {
assert.equal(null, false, 'null compare with false'); // 報錯
assert.equal(null, true, 'null compare with true'); // 報錯
assert.equal(undefined, false, 'undefined compare with false'); // 報錯
assert.equal(undefined, true, 'undefined compare with true'); // 報錯
assert.equal('', false, '"" compare with false'); // 正常
})
複製程式碼
notEqual(actual, expected[, message])
用法同 assert.equal(actual, expect[, message])
只是對預期結果取反(即不等於)。
assert.strictEqual(actual, expect[, message])
用法同 assert.equal(actual, expect[, message])
但是內部比較是使用的是 === 而不是 ==。
assert.notStrictEqual(actial, expected[, message])
用法同 assert.strictEqual(actual, expect[, message])
只是對預期結果取反(即不嚴格等於)。
it('assert.strictEqual', () => {
assert.strictEqual('', false); // 報錯
})
複製程式碼
assert.deepEqual(actual, expect[, message])
deepEqual 方法用於比較兩個物件。比較的過程是比較兩個物件的 key 和 value 值是否相同, 比較時用的是 == 而不是 ===。
it('assert.deepEqual', () => {
const a = { v: 'value' };
const b = { v: 'value' };
assert.deepEqual(a, b);
})
複製程式碼
assert.notDeepEqual(actual, expected[, message])
用法同 assert.deepEqual(actual, expect[, message])
只是對預期結果取反(即不嚴格深等於)。
assert.deepStrictEqual(actual, expect[, message])
用法同 assert.deepEqual(actual, expect[, message])
但是內部比較是使用的是 === 而不是 ==。
assert.notDeepStrictEqual(actual, expected[, message])
用法同 assert.deepStrictEqual(actual, expect[, message])
只是對結果取反(即不嚴格深等於)。
assert.throws(block[, error][, message])
錯誤斷言與捕獲, 斷言指定程式碼塊執行一定會報錯或丟擲錯誤。若程式碼執行未出現錯誤則會斷言失敗,斷言異常。
it('throws', () => {
var fun = function() {
xxx
};
assert.throws(fun, 'fun error');
})
複製程式碼
assert.doesNotThrow(block[, error][, message])
錯誤斷言與捕獲, 用法同 throws 類似,只是和 throws 預期結果相反。斷言指定程式碼塊執行一定不會報錯或丟擲錯誤。若程式碼執行出現錯誤則會斷言失敗,斷言異常。
it('throws', () => {
var fun = function() {
xxx
};
assert.doesNotThrow(fun, 'fun error');
})複製程式碼
相應的工具介紹之後,針對Mocha、Karma以及Travis.CI的用法談點個人在操作時的實踐經驗。
Mocha
- 安裝mocha
npm install mocha -g
複製程式碼
當然也可以在不在全域性安裝,只安區域性安裝在專案中
npm install mocha --save
複製程式碼
- 建立一個測試檔案
test.js
var assert = require('assert')
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal(-1, [1, 2, 3].indexOf(-1))
})
})
})
複製程式碼
這段檔案和簡單就是測試 Array
的一個 indexOf()
方法。這裡我是用的斷言庫是 Node 所提供的 Assert
模組裡的API。這裡斷言 -1 等於 陣列 [1, 2, 3]
執行 indexOf(-1)
後返回的值,如果測試通過則不會報錯,如果有誤就會報出錯誤。
下面我們使用全域性安裝的 mocha
來執行一下這個檔案 mocha test.js
。
下面是返回結果
基礎測試用例例項
const assert = require('assert');
describe('測試套件描述', function() {
it('測試用例描述: 1 + 2 = 3', function() {
// 測試程式碼
const result = 1 + 2;
// 測試斷言
assert.equal(result, 3);
});
});
複製程式碼
Mocha 測試用例主要包含下面幾部分:
- describe 定義的測試套件(test suite)
- it 定義的測試用例(test case)
- 測試程式碼
- 斷言部分
說明:每個測試檔案中可以有多個測試套件和測試用例。mocha不僅可以在node環境執行, 也可以在瀏覽器環境執行;在node中執行也可以通過npm i mocha -g
全域性安裝mocha然後以命令列的方式執行測試用例也是可行的。
這裡略微詳細介紹下測試指令碼寫法
Mocha 的作用是執行測試指令碼,首先必須學會寫測試指令碼。所謂"測試指令碼",就是用來測試原始碼的指令碼。下面是一個加法模組 add.js 的程式碼。
// add.js
function add(x, y) {
return x + y;
}
module.exports = add;
複製程式碼
要測試這個加法模組是否正確,就要寫測試指令碼。通常,測試指令碼與所要測試的原始碼指令碼同名,但是字尾名為.test.js(表示測試)或者.spec.js(表示規格)。比如,add.js 的測試指令碼名字就是 add.test.js。
// add.test.js
var add = require('./add.js');
var expect = require('chai').expect;
describe('加法函式的測試', function() {
it('1 加 1 應該等於 2', function() {
expect(add(1, 1)).to.be.equal(2);
});
});
複製程式碼
上面這段程式碼,就是測試指令碼,它可以獨立執行。測試指令碼里面應該包括一個或多個 describe 塊,每個 describe 塊應該包括一個或多個 it 塊。
describe 塊稱為"測試套件"(test suite),表示一組相關的測試。它是一個函式,第一個引數是測試套件的名稱("加法函式的測試"),第二個引數是一個實際執行的函式。
it 塊稱為"測試用例"(test case),表示一個單獨的測試,是測試的最小單位。它也是一個函式,第一個引數是測試用例的名稱("1 加 1 應該等於 2"),第二個引數是一個實際執行的函式。
expect 斷言的優點是很接近自然語言,下面是一些例子。
// 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect(foo).to.be.deep.equal({ bar: 'baz' });
// 布林值為true
expect('everthing').to.be.ok;
expect(false).to.not.be.ok;
// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(foo).to.be.an.instanceof(Foo);
// include
expect([1, 2, 3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');
// empty
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;
// match
expect('foobar').to.match(/^foo/);
複製程式碼
基本上,expect 斷言的寫法都是一樣的。頭部是 expect 方法,尾部是斷言方法,比如 equal、a/an、ok、match 等。兩者之間使用 to 或 to.be 連線。如果 expect 斷言不成立,就會丟擲一個錯誤。事實上,只要不丟擲錯誤,測試用例就算通過。
it('1 加 1 應該等於 2', function() {});
複製程式碼
上面的這個測試用例,內部沒有任何程式碼,由於沒有丟擲了錯誤,所以還是會通過。
Karma
基於 karma 測試常用的一些模組
模組安裝
# 基礎測試庫
npm install karma-cli -g
npm install karma mocha karma-mocha --save-dev
# 斷言庫
npm install should --save-dev
npm install karma-chai --save-dev
# 瀏覽器相關
npm install karma-firefox-launcher --save-dev
npm install karma-chrome-launcher --save-dev
複製程式碼
配置
這裡的配置主要關注的是karma.conf.js
的相關配置。如果要使用 karma 和 mocha 最好通過npm install karma-cli -g
全域性安裝karma-cli
。具體配置配置說明
需要注意的兩個欄位:
- singleRun: 如果值為 true, 則在瀏覽器執行完測試後會自動退出關閉瀏覽器視窗。singleRun的值我們可以更具執行環境來動態賦值, 可以啟動命令中新增
NODE_ENV
變數。 - browsers: 瀏覽器配置(可以配置多個瀏覽器); 如果瀏覽器無法啟動需要進行相關瀏覽器的配置。設定自啟動瀏覽器時候如果瀏覽器啟動失敗可能需要設定為
--no-sandbox
模式。
{
"browsers": ["Chrome", "ChromeHeadless", "ChromeHeadlessNoSandbox"],
"customLaunchers": {
"ChromeHeadlessNoSandbox": {
"base": "ChromeHeadless",
"flags": ["--no-sandbox"]
}
}
}
複製程式碼
或者
{
"browsers": ["Chrome_travis_ci"],
"customLaunchers": {
"Chrome_travis_ci": {
"base": "Chrome",
"flags": ["--no-sandbox"]
}
}
}複製程式碼
Github專案接入Travis.CI進行整合自動化測試的步驟
專案建立、完善專案功能和測試程式碼。
- 專案需求: 實現一個求和方法
- 測試: 通過
mocha
來測試完成的求和方法。
下面是專案結構,專案建立完成後通過 npm i mocha -D
安裝 mocha
模組。然後在本地執行 npm test
看是否能夠測試通過。如果能夠測試通過則說明我們的可以繼續下一步了。
建立 travis-ci 測試配置檔案
建立 travis-ci 配置檔案 .travis.yml
, 檔案內容。更多關於配置檔案的說明在travis官網可查詢
language: node_js
node_js:
- "node"
- "8.9.4"
複製程式碼
至此基本完成了專案開發和測試程式碼編寫的過程,下一步就可以接入 travis-ci 測試了。
接入 travis-ci
通過GitHub登入 travis-ci 的官網 www.travis-ci.org/
找到GitHub上剛才建立的需要測試的專案,並開啟測試
檢視測試過程,及時發現問題。
檢視測試狀態是否通過測試,如果未通過及時排查問題反覆修改;如果通過可以在專案文件中新增一個測試通過的標識。
總結
年初的時候我曾去面試一個公司,公司是一個創業公司,老闆是前百度首席架構師林仕鼎先生,公司名字叫愛雲校,主要業務是為基礎教育提供資料服務,有自己的平臺和產品。當天面試官問我的面試問題中最令我印象深刻的就是:以前你做前端的時候的單元測試所用的框架和工作流是什麼?當時說實話我很懵逼,懵逼到我甚至不知道在前端領域什麼是單元測試?要測試什麼?用的啥工具?一時語塞,我說以前沒有用過,也沒有做過前端測試。當時面試官老哥明顯臉上有些失望,但是最終的結果是公司錄用了我,我說出這個經歷的原因,其實想傳達的意思是:現在的前端開發早已不是早年的切圖、特效實現和視覺表現。現在的前端開發工程師更多的是你作為一個在整個專案產品開發團隊中理論上(其實也是事實上)離使用者最近的一個崗位,應不僅僅限於實現純傳統前端的功能,更需要基於明白客戶需求這個基礎上統籌好前端和後端的良好耦合,以及前端功能的準確無誤。根據自己的面試求職經歷,今天在這裡談到的前端單元測試的內容,個人認為在某些公司專案中,可能並不是必須的,但作為發展最快的IT領域之一,前端工程師掌握前端單元測試在未來時間只能是要求越來越硬性,而不是一直停留在可有可無瞭解範疇,因為這跟前端開發的發展趨勢息息相關。
但是同時,在我個人觀點範疇內,至少目前我還是堅持開發為主測試為輔的流程,對於像TDD這種單元測試指導開發流程,目前並不推崇。個人認為,這是一個很有創新性的方法論,也並不是現在完全不可行,個人認為只是可行的範疇還不夠寬,可行的條件要求還很嚴苛。所以相對於TDD,測試主導開發,對於目前準備進階的前端開發者,個人更建議,瞭解某種以後會使用的新趨勢和技術是有必要的,但作為技術人應該在學習新的、前衛的技術的同時不可迷失自我一味追求新技術,更重要的是要磨練當下的主流技能。相比於未來的單元測試主導開發流程,倒不如在目前這個時間節點精進基礎開發流程,比如讓自己的JS程式碼更專注於模組化和功能化的實現,這樣的同時也會讓單元測試更有效率,真正發揮目前單元測試對前端工程化的作用。