在這裏說一下前端開發的一個特色是更多的會涉及用戶界面,當開發規模達到必定程度時,幾乎註定了其複雜度會成倍的增加。css
不管是在代碼的初始搭建過程當中,仍是以後難以免的重構和修正bug過程當中,經常會陷入邏輯難以梳理、沒法掌握全局關聯的境地。html
而單元測試做爲一種「提綱挈領、保駕護航」的基礎手段,爲開發提供了「圍牆和腳手架」,能夠有效的改善這些問題。前端
做爲一種經典的開發和重構手段,單元測試在軟件開發領域被普遍承認和採用;前端領域也逐漸積累起了豐富的測試框架和最佳實踐。node
本文將按以下順序進行說明:react
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。webpack
簡單來講,單元就是人爲規定的最小的被測功能模塊。單元測試是在軟件開發過程當中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其餘部分相隔離的狀況下進行測試。es6
測試框架的做用是提供一些方便的語法來描述測試用例,以及對用例進行分組。web
斷言是單元測試框架中核心的部分,斷言失敗會致使測試不經過,或報告錯誤信息。面試
對於常見的斷言,舉一些例子以下:ajax
同等性斷言 Equality Asserts
比較性斷言 Comparison Asserts
類型性斷言 Type Asserts
條件性測試 Condition Test
斷言庫主要提供上述斷言的語義化方法,用於對參與測試的值作各類各樣的判斷。這些語義化方法會返回測試的結果,要麼成功、要麼失敗。常見的斷言庫有 Should.js, Chai.js 等。
爲某個特殊目標而編制的一組測試輸入、執行條件以及預期結果,以便測試某個程序路徑或覈實是否知足某個特定需求。
通常的形式爲:
it('should ...', function() { ... expect(sth).toEqual(sth); });
一般把一組相關的測試稱爲一個測試套件
通常的形式爲:
describe('test ...', function() { it('should ...', function() { ... }); it('should ...', function() { ... }); ... });
正如
spy
字面的意思同樣,咱們用這種「間諜」來「監視」函數的調用狀況
經過對監視的函數進行包裝,能夠經過它清楚的知道該函數被調用過幾回、傳入什麼參數、返回什麼結果,甚至是拋出的異常狀況。
var spy = sinon.spy(MyComp.prototype, 'componentDidMount'); ... expect(spy.callCount).toEqual(1);
有時候會使用
stub
來嵌入或者直接替換掉一些代碼,來達到隔離的目的
一個stub
可使用最少的依賴方法來模擬該單元測試。好比一個方法可能依賴另外一個方法的執行,然後者對咱們來講是透明的。好的作法是使用stub 對它進行隔離替換。這樣就實現了更準確的單元測試。
var myObj = { prop: function() { return 'foo'; } }; sinon.stub(myObj, 'prop').callsFake(function() { return 'bar'; }); myObj.prop(); // 'bar'
mock
通常指在測試過程當中,對於某些不容易構造或者不容易獲取的對象,用一個虛擬的對象來建立以便測試的測試方法
廣義的講,以上的 spy 和 stub 等,以及一些對模塊的模擬,對 ajax 返回值的模擬、對 timer 的模擬,都叫作 mock 。
用於統計測試用例對代碼的測試狀況,生成相應的報表,好比 istanbul
是常見的測試覆蓋率統計工具
不一樣於"傳統的"(其實也沒出現幾年)的 jasmine / Mocha / Chai 等前端測試框架 -- Jest
的使用更簡單,而且提供了更高的集成度、更豐富的功能。
Jest 是 Facebook 出品的一個測試框架,相對其餘測試框架,其一大特色就是就是內置了經常使用的測試工具,好比自帶斷言、測試覆蓋率工具,實現了開箱即用。
此外, Jest 的測試用例是並行執行的,並且只執行發生改變的文件所對應的測試,提高了測試速度。
編寫單元測試的語法一般很是簡單;對於jest
來講,因爲其內部使用了 Jasmine 2
來進行測試,故其用例語法與 Jasmine 相同。
實際上,只要先記這住四個單詞,就足以應付大多數測試狀況了:
describe
: 定義一個測試套件it
:定義一個測試用例expect
:斷言的判斷條件toEqual
:斷言的比較結果describe('test ...', function() { it('should ...', function() { expect(sth).toEqual(sth); expect(sth.length).toEqual(1); expect(sth > oth).toEqual(true); }); });
Jest 號稱本身是一個 「Zero configuration testing platform」,只需在 npm scripts
裏面配置了test: jest
,便可運行npm test
,自動識別並測試符合其規則的(通常是 __test__
目錄下的)用例文件。
實際使用中,適當的自定義配置一下,會獲得更適合咱們的測試場景:
//jest.config.js module.exports = { modulePaths: [ "<rootDir>/src/" ], moduleNameMapper: { "\.(css|less)$": '<rootDir>/__test__/NullModule.js' }, collectCoverage: true, coverageDirectory: "<rootDir>/src/", coveragePathIgnorePatterns: [ "<rootDir>/__test__/" ], coverageReporters: ["text"], };
在這個簡單的配置文件中,咱們指定了測試的「根目錄」,配置了覆蓋率(內置的istanbul
)的一些格式,並將本來在webpack中對樣式文件的引用指向了一個空模塊,從而跳過了這一對測試無傷大雅的環節
//NullModule.js module.exports = {};
另外值得一提的是,因爲jest.config.js
是一個會在npm
腳本中被調用的普通 JS 文件,而非XXX.json
或.XXXrc
的形式,因此 nodejs 的各自操做均可以進行,好比引入 fs 進行預處理讀寫等,靈活性很是高,能夠很好的兼容各類項目
因爲是面向src
目錄下測試其React代碼,而且還使用了ES6語法,因此項目下須要存在一個.babelrc
文件:
{ "presets": ["env", "react"] }
以上是基本的配置,而實際因爲webpack能夠編譯es6的模塊,通常將babel中設爲{ "modules": false }
,此時的配置爲:
//package.json "scripts": { "test": "cross-env NODE_ENV=test jest", },
//.babelrc { "presets": [ ["es2015", {"modules": false}], "stage-1", "react" ], "plugins": [ "transform-decorators-legacy", 若是對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。 "react-hot-loader/babel" 感興趣能夠175317069,羣內會有不按期的發放免費的資料連接,這些資料都是從各 ], 個技術網站蒐集、整理出來的,若是你有好的學習資料能夠私聊發我,我會註明出處 "env": { 以後分享給你們。 "test": { "presets": [ "es2015", "stage-1", "react" ], "plugins": [ "transform-decorators-legacy", "react-hot-loader/babel" ] } } }
Enzyme 來自於活躍在 JavaScript 開源社區的 Airbnb 公司,是對官方測試工具庫(react-addons-test-utils)的封裝。
這個單詞的倫敦讀音爲 ['enzaɪm]
,酵素或酶的意思,Airbnb 並無給它設計一個圖標,估計就是想取用它來分解 React 組件的意思吧。
它模擬了 jQuery 的 API,很是直觀而且易於使用和學習,提供了一些不同凡響的接口和幾個方法來減小測試的樣板代碼,方便判斷、操縱和遍歷 React Components 的輸出,而且減小了測試代碼和實現代碼之間的耦合。
通常使用 Enzyme 中的 mount
或 shallow
方法,將目標組件轉化爲一個 ReactWrapper
對象,並在測試中調用其各類方法:
import Enzyme,{ mount } from 'enzyme'; ... describe('test ...', function() { it('should ...', function() { wrapper = mount( <MyComp isDisabled={true} /> ); expect( wrapper.find('input').exists() ).toBeTruthy(); }); });
圖中這位「我牽着馬」的並非捲簾大將沙悟淨...其實圖中的故事正是人所皆知的「特洛伊木馬」;大概意思就是希臘人圍困了特洛伊人十多年,久攻不下,心生一計,把營盤都撤了,只留下一個巨大的木馬(裏面裝着士兵),以及這位被扒光還被打得夠嗆的人,也就是此處要談的主角sinon,由他欺騙特洛伊人 --- 後面的劇情你們就都熟悉了。
因此這個命名的測試工具呢,也正是各類假裝滲透方法的合集,爲單元測試提供了獨立而豐富的 spy, stub 和 mock 方法,兼容各類測試框架。
雖然 Jest 自己也有一些實現 spy 等的手段,但 sinon 使用起來更加方便。
這裏不展開討論經典的 「測試驅動開發」(TDD - test driven development) 理論 -- 簡單的說,把測試正向加諸開發,先寫用例再逐步實現,就是TDD,這是很好理解的。
而當咱們反過頭來,對既有代碼補充測試用例,使其測試覆蓋率不斷提升,並在此過程當中改善原有設計,修復潛在問題,同時又保證原有接口不收影響,這種 TDD 行爲雖然沒人稱之爲「測試驅動重構」(test driven refactoring),但「重構」這個概念自己就包含了用測試保駕護航的意思,是必不可少的題中之意。
對於一些組件和共有函數等,完善的測試也是一種最好的使用說明書。
因爲測試結果中,成功的用例會用綠色表示,而失敗的部分會顯示爲紅色,因此單元測試也經常被稱爲 「Red/Green Testing」 或 「Red/Green Refactoring」 , 這也是 TDD 中的通常性步驟:
若是對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。感興趣能夠175317069,羣內會有不按期的發放免費的資料連接,這些資料都是從各個技術網站蒐集、整理出來的,若是你有好的學習資料能夠私聊發我,我會註明出處以後分享給你們。
這就是 jest
內置的 istanbul
輸出的覆蓋率結果。
之因此叫作「伊斯坦布爾」,是由於土耳其地毯世界聞名,而地毯是用來"覆蓋"的♀️。
表格中的第2列至第5列,分別對應四個衡量維度:
if
代碼塊都執行了測試結果根據覆蓋率被分爲「綠色、黃色、紅色」三種,應該視具體狀況儘可能提升相應模塊的測試覆蓋率。
合理編寫組件化的 React,並將足夠獨立、功能專注的組件做爲測試的單元,將使得單元測試變得容易;
反之,測試的過程讓咱們更易釐清關係,將本來的組件重構或分解成更合理的結構。分離出的子組件每每也更容易寫成stateless
的無狀態組件,使得性能和關注點更加優化。
對於一些以前定義並不清晰的組件,能夠統一引入 prop-types
,明確組件可接收的props
;一方面能夠在開發/編譯過程當中隨時發現錯誤,另外也能夠在團隊中其餘成員引用組件時造成一個明晰的列表。
能夠用beforeEach
和afterEach
作一些統一的預置和藹後工做,在每一個用例的以前和以後都會自動調用:
describe('test components/Comp', function() { let wrapper; let spy; beforeEach(function() { jest.useFakeTimers(); spy = sinon.spy(Comp.prototype, 'componentDidMount'); }); afterEach(function() { jest.useRealTimers(); wrapper && wrapper.unmount(); didMountSpy.restore(); didMountSpy = null; }); it('應該正確顯示基本結構', function() { wrapper = mount( <Comp ... /> ); expect(wrapper.find('a').text()).toEqual('HELLO!'); }); ... });
對於一些組件中,若是但願在測試階段調用到其一些內部方法,又不想對原組件改動過大的,能夠用instance()
取得組件類實例:
it('應該正確獲取組件類實例', function() { var wrapper = mount( <MultiSelect name="HELLOKITTY" placeholder="select sth..." /> ); var wi = wrapper.instance(); expect( wi.props.name ).toEqual( "HELLOKITTY" ); expect( wi.state.open ).toEqual( false ); });
做爲UI組件,React組件中一些操做須要延時進行,諸如onscroll
或oninput
這類高頻觸發動做,須要作函數防抖或節流,好比經常使用的 lodash 的 debounce 等。
所謂的異步操做,在不考慮和 ajax 整合的集成測試的狀況下,通常都是指此類操做,只用 setTimeout 是不行的,須要搭配 done
函數使用:
//組件中 const Comp = (props)=>( <input type="text" id="searchIpt" onChange={ debounce(props.onSearch, 500) } /> );
//單元測試中 it('應該在輸入時觸發回調', function(done) { var spy = jest.fn(); var wrapper = mount( <Comp onChange={ spy } /> ); wrapper.find('#searchIpt').simulate('change'); setTimeout(()=>{ expect( spy ).toHaveBeenCalledTimes( 1 ); done(); }, 550); });
一些模塊中可能耦合了對 window.xxx
這類全局對象的引用,而徹底去實例化這個對象可能又牽扯出不少其餘的問題,難以進行;此時能夠見招拆招,只模擬一個最小化的全局對象,保證測試的進行:
//fakeAppFacade.js var facade = { router: { current: function() { return {name:null, params:null}; } }, appData: { symbol: "¥" } }; window._appFacade = facade; module.exports = facade;
//測試套件中 import fakeFak from '../fakeAppFacade';
另外好比 LocalStroage 這類對象,測試端環境中沒有原生支持,也能夠簡單模擬一下:
//fakeStorage.js var _util = {}; var fakeStorage = { "set": function(k, v) { _util['_fakeSave_'+k] = v; }, "get": function(k) { return _util['_fakeSave_'+k] || null; }, "remove": function(k) { delete _util['_fakeSave_'+k]; }, "has": function(k) { return _util.hasOwnProperty('_fakeSave_'+k); } }; module.exports = fakeStorage;
在一個項目中用到了 react-bootstrap
界面庫,測試一個組件時,因爲包含了其 Modal
模態彈窗,而彈窗組件是默認渲染到 document
中的,致使難以用普通的 find
方法等獲取
解決的辦法是模擬一個渲染到容器組件原處的普通組件:
//FakeReactBootstrapModal.js import React, {Component} from 'react'; class FakeReactBootstrapModal extends Component { constructor(props) { super(props); } render() { //原生的 react-bootstrap/Modal 沒法被 enzyme 測試 const { show, bgSize, dialogClassName, children } = this.props; return show ? <div className={ `fakeModal ${bgSize} ${dialogClassName}` }>{children}</div> : null; } } export default FakeReactBootstrapModal;
同時在組件渲染時,加入判斷邏輯,使之能夠支持自定義的類代替 Modal 類:
//ModalComp.js import { Modal } from 'react-bootstrap'; ... render() { const MyModal = this._modalClass || Modal; return (<MyModal bsSize={props.mode>1 ? "large" : "middle"} dialogClassName="custom-modal"> ... </MyModal>; }
而測試套件中,實現一個測試專用的子類:
//myModal.spec.js import ModalComp from 'components/ModalComp'; class TestModalComp extends ModalComp { constructor(props) { super(props); this._modalClass = FakeReactBootstrapModal; } }
這樣測試便可順利進行,跳過了並不重要的 UI 效果,而各類邏輯都能被覆蓋了
若是對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。感興趣能夠175317069,羣內會有不按期的發放免費的資料連接,這些資料都是從各個技術網站蒐集、整理出來的,若是你有好的學習資料能夠私聊發我,我會註明出處以後分享給你們。
在單元測試的過程當中,不免碰到一些須要遠程請求數據的狀況,好比組件獲取初始化數據、提交變化數據等。
要注意這種測試的目的仍是考察組件自己的表現,而非重點關心實際遠程數據的集成測試,因此咱們無需真實的請求,能夠簡單的模擬一些請求的場景。
sinon 中有一些模擬 XMLHttpRequest 請求的方法, jest 也有一些第三方的庫解決 fetch 的測試;
在咱們的項目中,根據實際的用法,本身實現一個類來模擬請求的響應:
//FakeFetch.js import { noop } from 'lodash'; const fakeFetch = (jsonResult, isSuccess=true, callback=noop)=>{ const blob = new Blob( [JSON.stringify(jsonResult)], {type : 'application/json'} ); return (...args)=>{ console.log('FAKE FETCH', args); callback.call(null, args); return isSuccess ? Promise.resolve( new Response( blob, {status:200, statusText:"OK"} ) ) : Promise.reject( new Response( blob, {status:400, statusText:"Bad Request"} ) ) } }; export default fakeFetch;
//Comp.spec.js import fakeFetch from '../FakeFetch'; const _fc = window.fetch; //緩存「真實的」fetch describe('test components/Comp', function() { let wrapper; afterEach(function() { wrapper && wrapper.unmount(); window.fetch = _fc; //恢復 }); it("應該在遠程請求時響應onRemoteData", (done)=>{ window.fetch = fakeFetch({ brand: "GoBelieve", tree: { node: '總部', children: null } }); let spy = jest.fn(); wrapper = mount( <Comp onRemoteData={ spy } /> ); jest.useRealTimers(); _clickTrigger(); //此時應該發起請求 setTimeout(()=>{ expect(wrapper.html()).toMatch(/總部/); expect(spy).toHaveBeenCalledTimes(1); done(); }, 500); }); });
單元測試做爲一種經典的開發和重構手段,在軟件開發領域被普遍承認和採用;前端領域也逐漸積累起了豐富的測試框架和方法。
單元測試能夠爲咱們的開發和維護提供基礎保障,使咱們在思路清晰、心中有底的狀況下完成對代碼的搭建和重構;
須要注意的是,世上沒有包治百病的良藥,單元測試也毫不是萬金油,秉持謹慎認真負責的態度才能從根本上保證咱們工做的進行。