測試
為什么項目需要測試
測試是完善的研發體系中不可或缺的一環,前端同樣需要測試。一個項目最終會經過快速迭代走向以維護為主的狀態,在合理的時機以合適的方式引入自動化能夠讓我們提前發現 bug,此時定位和修復的速度比開發完再被叫去修改 bug 要快許多;在項目重構或者開發人員發生變化也能保障預期功能的實現。
可測試方向
- 界面回歸測試: 測試界面是否正常,這是前端測試最基礎的環節
- 功能測試: 測試功能操作是否正常,由於涉及交互,這部分測試比界面測試會更復雜
- 性能測試: 頁面性能越來越受到關注,並且性能需要在開發過程中持續關注,否則很容易隨着業務迭代而下降
- 頁面特征檢測: 有些動態區域無法通過界面對比進行測試、也沒有功能上的異常,但可能不符合需求。例如性能測試中移動端大圖素材檢測就是一種特征檢測,另外常見的還有頁面區塊靜態資源是否符合預期等等。
前端測試框架
前端測試工具也和前端的框架一樣紛繁復雜,其中常見的測試工具,大致可分為測試框架、斷言庫、測試覆蓋率工具等幾類。常見的測試框架有Jasmine
, Mocha
, 以及要介紹的 Jest
。
測試框架的作用是提供一些方便的語法來描述測試用例,以及對用例進行分組。
測試框架可分為兩種,TDD (測試驅動開發)和 BDD (行為驅動開發)。
前端是一種特殊的GUI軟件.
斷言庫
所謂斷言,即提供語義化的方法,用於對參與測試的值做各種各樣的判斷,如不一致就拋出錯誤。常見的斷言庫有 should.js
, Chai.js
所有的測試用例(it塊)都應該含有一句或多句的斷言。它是編寫測試用例的關鍵
1
|
expect(add(1, 1)).to.be.equal(2);
|
Jest
Jest 內置了常用的測試工具,如斷言、測試覆蓋率。
命名慣例
測試文件有如下常見的命名慣例。
__tests__
目錄下以.js
為后綴的文件。- 以
.test.js(x)
或者.spec.js(x)
為后綴的文件。
測試文件可以位於項目根目錄下任何位置,可以通過 testMatch
修改默認配置。
編寫測試
Jest 的作用是運行測試腳本。通常,測試腳本與所要測試的源碼腳本同名,但是后綴名為 .test.js
。測試腳本可以獨立運行。
測試腳本里包含一個或多個 describe
塊, 每個 describe
里應該包含一個或多個 test
塊。
1
2
3
4
5
6
7
8
9
10
11
12
|
/* add.js */
function add(x, y) {
return x + y;
}
/* add.test.js */
const add = require('./add.js');
describe('加法函數的測試', function() {
it('1 加 1 應該等於 2', function() {
expect(add(1, 1)).toBe(2);
});
});
|
Jest 提供了內置的全局函數 expect
進行斷言。
describe
稱為測試套件(test suite),表示一組相關的測試。第一個參數是測試套件的名稱,第二個參數是實際執行的函數。
test
稱為測試用例(test case),表示一個單獨的測試,是測試的最小單位。第一個參數是測試用例的名稱,第二個參數是實際執行的函數。
1
2
3
|
it('work without done', () => {}); // 同步執行
it('work with done', (done) => {}); // 觸發異步,執行 done() 通知 Jest 之行完畢
|
異步測試
使用單個參數調用 done
, Jest 會等 done
回調函數執行結束后,結束測試。如果 done()
永遠不被調用,這個測試將失敗。
1
2
3
4
5
6
7
8
9
|
it('works with done', (done) => {
var x = true;
var f = function() {
x = false;
expect(x).toBeFalsy();
done(); // 通知 Jest 測試結束
};
setTimeout(f, 4000);
});
|
Jest 支持使用 Promise, 從測試返回一個 Promise, Jest 會等待這一 Promise resolve。 如果 Promise 被拒絕,則測試將自動失敗。
1
2
3
4
5
6
7
8
9
10
11
12
|
const requestFn = (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('name');
}, 1000);
})
}
it('works with promises', () => {
expect.assertions(1); // 當前測試中執行斷言的次數
return requestFn('mxl').then(data => expect(data).toBe('name'));
});
|
Jest 也支持 async/await 語法的測試,無需多余的操作,只要在 await 后進行斷言即可。
可以使用 expect.assertions
來驗證一定數量的斷言被調用,以判斷異步代碼是否如預期一般執行。
測試組件
冒煙測試 驗證一個組件渲染沒有拋出異常,淺渲染並且測試一些輸出,完整渲染測試組件的生命周期和狀態的改變。
1
2
3
4
5
6
7
8
|
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});
|
初始化測試環境
使用 browser API 需要 mock,或者在測試前運行全局的配置,可以在 setup.js
文件里配置。
測試用例鈎子
有時我們想在測試開始之前進行下環境的檢查、或者在測試結束之后作一些清理操作,這就需要對用例進行預處理或后處理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
describe('hooks', function() {
beforeAll(function() {
// 在本區塊的所有測試用例之前執行
});
afterAll(function() {
// 在本區塊的所有測試用例之后執行
});
beforeEach(function() {
// 在本區塊的每個測試用例之前執行
});
afterEach(function() {
// 在本區塊的每個測試用例之后執行
});
// test cases
});
|
測試用例管理
項目中測試用例很多,但希望只運行其中的幾個,可以用only方法,describe
塊和 test
塊都允許調用 only
方法,表示只運行某個測試套件或測試用例。
1
2
|
it.only('1 加 1 應該等於 2', () => { ... });
fit('1 加 1 應該等於 2', () => { ... });
|
此外,還有 skip
方法,表示跳過指定的測試套件或測試用例。
1
2
|
it.skip('1 加 1 應該等於 2', () => { ... });
xit('1 加 1 應該等於 2', () => { ... });
|
覆蓋率報告
Jest 匹配文件生成測試報告,不需要額外的配置。
除了會再終端展示測試覆蓋率情況,還會在項目下生產一個 coverage 目錄。
1
|
npm test -- --coverage
|
小結
對於一些需求頻繁變更、復用性較低的內容,編寫測試用例得不償失,適合引入測試用例的場景如下:
- 需要長期維護的項目。它們需要測試來保障代碼可維護性、功能的穩定性
- 較為穩定的項目、或項目中較為穩定的部分。給它們寫測試用例,維護成本低
- 被多次復用的部分,比如一些通用組件和庫函數。因為多處復用,更要保障質量
參考文獻
react-test-demo(git上挺好的中文講解)
jest(中文)
jest(英文)
Jest Snapshots and Beyond - React Conf 2017
The Difference Between TDD and BDD