- 蘇格團隊
- 作者:Dee
前言
單元測試的好處:
- 可保證得到結果的一致性,提高專案、元件穩定性。
- 開發者按單元測試思路去寫程式碼,可清晰程式碼結構,提高程式碼的可讀性。
由於筆者開發的專案越來越大,公共元件的複用性高,故其穩定性尤為重要。因此,引入單元測試刻不容緩。
單元測試的不好:
- 會佔用一定的開發成本,增加開發工作量。
- 舊專案加入單元測試改動很大,會有一定的風險。
- 會有一定的學習成本,對開發者要求比較高。
- 如果在一些複用性很低的元件使用單元測試,成效不大且開發成本高。
選型
在做專案單元測試前,筆者參考了網上的一些文章以及官方文件,最後選型為Jest + react-test-renderer + Enzyme。
-
Jest
Jest 是 Facebook 出品的一個測試框架,相對其他測試框架,其一大特點就是就是整合了 Mocha,chai,jsdom,sinon等功能,內建了常用的測試工具,比如自帶斷言、測試覆蓋率工具,實現了開箱即用。
-
react-test-render
配合react-test-render,Jest 可提供了快照測試功能。
首次執行快照測試,會產生一個可讀的快照,再次測試時會通過比對快照檔案和新產生的快照判斷測試是否通過。
Jest在執行的時候如果發現toMatchSnapshot方法,會在同級目錄下生成一個__ snapshots__資料夾用來存放快照檔案,以後每次測試的時候都會和第一次生成的快照進行比較。
-
Enzyme
React官方已經提供了一個測試工具庫:react-dom/test-utils。但是用起來不夠方便,於是有了一些第三方的封裝庫,比如Airbnb公司的Enzyme。其兩大特點:
- 提供了一套簡潔強大的 API,並內建Cheerio
- 實現了jQuery風格的方式進行DOM 處理,開發體驗十分友好
三種渲染方法
shallow:淺渲染,是對官方的Shallow Renderer的封裝。將元件渲染成虛擬DOM物件,只會渲染第一層,子元件將不會被渲染出來,使得效率非常高。不需要DOM環境, 並可以使用jQuery的方式訪問元件的資訊
render:靜態渲染,它將React元件渲染成靜態的HTML字串,然後使用Cheerio這個庫解析這段字串,並返回一個Cheerio的例項物件,可以用來分析元件的html結構
mount:完全渲染,它將元件渲染載入成一個真實的DOM節點,用來測試DOM API的互動和元件的生命週期。用到了jsdom來模擬瀏覽器環境
三種方法中,shallow和mount因為返回的是DOM物件,可以用simulate進行互動模擬,而render方法不可以。一般shallow方法就可以滿足需求,如果需要對子元件進行判斷,需要使用render,如果需要測試元件的生命週期,需要使用mount方法。
注意:enzyme還需要根據React的版本安裝介面卡,介面卡對應表如下:
方案
前面說了這麼多,是時候上程式碼了。
-
目錄
筆者在根目錄新建一個unitTest目錄,其目錄結構為:
-
jest.config.js:jest配置檔案
-
mocks:mock檔案目錄
-
components:專案的公共元件單元測試用例目錄
-
components/__ snapshots __:執行單元測試時自動生成的快照存放目錄
-
-
安裝(由於筆者是react16版本,所以安裝的介面卡版本為enzyme-adapter-react-16)
npm install jest enzyme enzyme-adapter-react-16 react-test-renderer
-
配置
Jest支援直接在package.json檔案寫入配置,但筆者有輕微潔癖,喜歡把配置檔案寫到unitTest裡面,方便查詢以及閱讀。
// package.json { "scripts": { "jest": "jest --config ./unitTest/jest.config.js", // 單元測試 "jestupdate": "jest --config ./unitTest/jest.config.js --updateSnapshot" // 單元測試快照更新 "jestreport": "jest --config ./unitTest/jest.config.js --coverage" // 單元測試並生成覆蓋率報告 } } 複製程式碼
// jest.config.js module.exports = { testURL: 'http://localhost/', setupFiles: [], moduleFileExtensions: ['js', 'jsx'], testPathIgnorePatterns: ['/node_modules/'], testRegex: '.*\\.test\\.js$', collectCoverage: false, collectCoverageFrom: ['src/components/**/*.{js}'], moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/mocks/fileMock.js', '\\.(css|less|scss)$': '<rootDir>/mocks/styleMock.js' } }; 複製程式碼
- testURL: jsdom執行url,預設為"about:blank",如果不設定,會在嘗試訪問localStorage出錯。
- setupFiles:執行測試程式碼前,Jest會先執行setupFile指定的配置檔案來初始化測試環境。
- moduleFileExtensions:支援單元測試的副檔名。
- testPathIgnorePatterns:匹配忽略檔案規則。
- testRegex:匹配測試檔案規則。
- collectCoverage:是否生成測試覆蓋報告,開啟會增加測試時間。
- collectCoverageFrom:指示應收集覆蓋率資訊的一組檔案。如果檔案與指定的glob模式匹配,即使此檔案不存在測試,也將為其收集覆蓋率資訊,並且測試套件中從不需要它。
- moduleNameMapper:可用於將模組路徑對映到不同的模組。預設情況下,預設將所有影像對映到影像存根模組,但如果找不到模組,可配置此選項。
-
mock檔案
// fileMock.js module.exports = {}; 複製程式碼
// styleMock.js module.exports = {}; 複製程式碼
-
編寫單元測試
// button.test.js import Button from '../../src/common/components/Button'; import renderer from 'react-test-renderer'; import React from 'react'; import { shallow, configure } from 'enzyme'; // shallow(淺渲染,只渲染父元件) import Adapter from 'enzyme-adapter-react-16'; // 適應React-16 configure({ adapter: new Adapter() }); // 適應React-16,初始化 const props = { text: '按鈕測試用例', type: 'white', style: { marginTop: 15 }, size: 'big', disabled: false, height: 'middle', isLock: true, cname: 'hello', onClick: () => {} }; describe('test Button', () => { it('button render correctly', () => { const tree = renderer.create(<Button {...props} />).toJSON();// 生成快照 expect(tree).toMatchSnapshot(); // 匹配之前的快照 }); it('button has class', () => { const item = shallow(<Button {...props} />); //淺渲染 expect(item.hasClass('hello')).toBe(true); // 斷言有item有hello的className }); }); 複製程式碼
後記
注意事項:
1、如果不配置testURL,會報錯:localStorage is not available for opaque origins
2、本文件只講述筆者的實踐方案以供參考,關於Jest、enzyme的具體介紹、用法可參考