You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constisPass=require("./pass");//describe中通常寫一個元件或是一個functiondescribe("function isPass()",()=>{//it是這個元件或function中的test caseit("should return true when score is 60",()=>{expect(isPass(60)).toBe(true);// 期待isPass(60)回傳的結果是true});it("should return true when score is 45",()=>{expect(isPass(45)).toBe(false);});});
constchoice12=require('./choice12');describe('function choice12()',()=>{it('should return 1 when enter 1',()=>{expect(choice12(1)).toBe(1);});it('should return 2 when enter 2',()=>{expect(choice12(2)).toBe(2);});// it('should return 0 when enter 5', () => {// expect(choice123(5)).toBe(0);// });});
Snapshot testing is another new idea from Facebook. It provides an alternate way to write tests without any assertions. To write tests using assertions, Enzyme is quite useful.
如過只要測試單一的test.js檔案,像是我如果想執行/app/components/choice資料夾底下的測試程式,就在指令的地方下jest /app/components/choice,就會只測試對應到的檔案,在cmd上會顯示Ran all test suites matching "/app/components/choice".,同樣的,如果只要更新相關檔案的snap檔,就下jest /app/components/choice -u就可以了。
以上的斷言基本是用在測試同步函數的返回值,如果所測試的函數存在異步邏輯。那麼在測試時就應該利用jest 的mock function 來進行測試。通過mock function 可以輕鬆地得到回調函數的調用次數、參數等調用信息,而不需要編寫額外的代碼去獲取相關數據,讓測試用例變得更可讀。
// 輸入一個數字,它會回你平方跟乘以3的數字functiongetDoubleAndMultiplyby3(val,callback){if(val<0){return;}callback(val*val,val*3);}constmockFn=jest.fn();getDoubleAndMultiplyby3(5,mockFn);getDoubleAndMultiplyby3(10,mockFn);describe("testing getDouble() with a mock function",()=>{it("it should was called least once",()=>{// expect(mockFn).toHaveBeenCalled();expect(mockFn).toBeCalled();});it("it should was called twice",()=>{expect(mockFn).toHaveBeenCalledTimes(2);});it("it should was return 25,15 in first callback",()=>{// expect(mockFn).toHaveBeenCalledWith(25,15);expect(mockFn).toHaveBeenCalledWith(25,15);});it("it should was return 100,30 in last callback",()=>{// expect(mockFn).toHaveBeenLastCalledWith(100,30);expect(mockFn).lastCalledWith(100,30);});});
import{shallow}from'enzyme';importMyComponentfrom'./MyComponent';importFoofrom'./Foo';describe('<MyComponent />',()=>{// 檢查是否成功渲染一個MyComponentit('renders <MyComponent /> components',()=>{constwrapper=shallow(<MyComponent/>);expect(wrapper).toHaveLength(1);// expect(wrapper).exists()); //應該與上一行效果一樣});// 檢查MyComponent中是否成功渲染3個Foo it('renders three <Foo /> components',()=>{constwrapper=shallow(<MyComponent/>);expect(wrapper.find(Foo)).toHaveLength(3);});// 檢查MyComponent中是否成功渲染3個Foo it('counter+1 when the last button is clicked',()=>{constwrapper=shallow(<MyComponent/>);//找出wrapper中的最後一個button,並且用滑鼠點擊wrapper.find('button').last().simulate('click');// ... expect(counter).toBe(1);});});
it('set props to error, Snackbar should be opened',async()=>{const{ wrapper, dispatch }=setup();expect(wrapper.find(Snackbar).prop('open')).toBeFalsy();wrapper.setProps({error: 'RequestToken.Invalid'});expect(wrapper.find(Snackbar).prop('open')).toBeTruthy();awaitsleep(2050);// wait for more than 2000 ms(autoHideDuration)expect(dispatch).toHaveBeenCalledWith({error: '',type: 'app/Main/REQUEST_ERROR'});wrapper.setProps({error: ''});expect(wrapper.find(Snackbar).prop('open')).toBeFalsy();});
importReactfrom'react';import{shallow,mount}from'enzyme';import{IntlProvider,intlShape}from'react-intl';// mock IntlProvider in LanguageProviderimportgetMuiThemefrom'material-ui/styles/getMuiTheme';// mock MuiThemeProviderimportReactRouterEnzymeContextfrom'react-router-enzyme-context';// mock ConnectedRouterimportconfigureStorefrom'redux-mock-store';// mock Provider// set up Providerconststore=configureStore([])({});store.dispatch=jest.fn();constdispatch=store.dispatch;// set up intlProviderconstmessages=require('translations/en.json');constintlProvider=newIntlProvider({locale: 'en', messages },{});const{ intl }=intlProvider.getChildContext();// set up MuiThemeProviderconstmuiTheme=getMuiTheme();// set up ConnectedRouterconstrouter=newReactRouterEnzymeContext().get().context.router;// assign props 'dispatch' into node, node is the component which we want to test.functionnodeWithProps(node){returnReact.cloneElement(node,{ dispatch });}// pass down the context of Provider, intlProvider, MuiThemeProvider and ConnectedRouter to shallowexportfunctionshallowWithProviders(node){returnshallow(nodeWithProps(node),{context: { intl, muiTheme, router, store }});}// pass down the context of Provider, intlProvider, MuiThemeProvider and ConnectedRouter to mountexportfunctionmountWithProviders(node){returnmount(nodeWithProps(node),{context: { intl, muiTheme, router, store },childContextTypes: {intl: intlShape,muiTheme: React.PropTypes.object,router: React.PropTypes.object,store: React.PropTypes.object,},});}
enzymeHelper.js的使用方法
importReactfrom'react';import{mountWithProviders}from'enzymeHelper';constdefaultProps={'test': "testVaule"}constsetup=(props={})=>{constwrapper=mountWithProviders(<MyContainer{...defaultProps}{...props}/>);constactions={testMethod: wrapper.instance().testMethod,testOtherMethod: wrapper.instance().testOtherMethod,mock: (...methods)=>{//you need to implement mock() in every test.jsmethods.forEach((method)=>{wrapper.instance()[method]=jest.fn();actions[method]=wrapper.instance()[method];});},};return{
wrapper,
actions,dispatch: wrapper.props().dispatch,// customize common DOM you want to test// ex: loginBtn: wrapper.find(FlatButton).at(0),}};describe('functions of <MyComponent />',()=>{it('testMethod()',()=>{const{ wrapper, actions, dispatch }=setup();// testMethod() include a dispatchactions.testMethod();// check the testMethod() trigger dispatch or not expect(dispatch).toHaveBeenCalledWith({data: {testContent: null,type: 'Text'},type: 'app/MyComponent/TEST_METHOD',});it('click button to trigger testMethod()',()=>{const{ wrapper, actions }=setup();// mock testMethod()actions.mock('testMethod');/* * if you want to mock multiple functions * you can assign mutiple parameters into actions.mock() * ex: actions.mock('testMethod','testOtherMethod'); */wrapper.find('button').first().simulate('click');// testMethod() will be called once after Clicking buttonexpect(actions.testMethod).toHaveBeenCalledTimes(1);});
直接使用下方的Template開始開發Container的單元測試
// *** REMOVE THE COMMENTS IN THIS FILE TO START CODING UNIT TEST ***/* * ComponentName <= replace ComponentName with the Component you want to test. * FunctionName <= replace FunctionName with the function you want to test. */importReactfrom'react';import{shallow}from'enzyme';import{mountWithProviders}from'enzymeHelper';/* TODO: import the component you want like material-ui TextField or FlatButton */import{ComponentName}from'../index';constdefaultProps={/* TODO: need to put default props here */};constsetup=(props={})=>{constwrapper=mountWithProviders(<ComponentName{...defaultProps}{...props}/>);constactions={/* TODO: customize common DOM you want to test */mock: (...methods)=>{methods.forEach((method)=>{wrapper.instance()[method]=jest.fn();actions[method]=wrapper.instance()[method];});},};return{
wrapper,
actions,dispatch: wrapper.props().dispatch,/* TODO: customize common DOM you want to test */};};describe('<ComponentName />',()=>{it('match snapshot',()=>{constwrapper=shallow(<ComponentName{...defaultProps}/>);expect(wrapper).toMatchSnapshot();});it('must have ......',()=>{const{ wrapper }=setup();// TODO: check if the important DOM exists ex: button, input, table// ex: expect(wrapper.find(FlatButton)).toHaveLength(1);});// TODO: check events executing correctly or not, ex: click,change,keypress// TODO: check UI changing correctly or not when different props assign});describe('functions of <ComponentName />',()=>{it('FunctionName()',()=>{const{ actions }=setup();// if you want to mock FunctionName(), then use actions.mock('FunctionName');actions.FunctionName();// TODO: check the result correct or not });// TODO: test other functions});
Component的單元測試Template
//直接使用下方的Template開始開發Container的單元測試importReactfrom'react';import{shallow}from'enzyme';// TODO: import the component you want like material-ui TextField or FlatButtonconstdefaultProps={// TODO: need to put default props here};importCheckboxOptionfrom'../index';describe('<CheckboxOption />',()=>{it('match snapshot and must have ......',()=>{constwrapper=shallow(<CheckboxOption{...defaultProps}/>);// TODO: check if the important DOM exists ex: button, input, table// ex: expect(wrapper.find(FlatButton)).toHaveLength(1);});// TODO: check events executing correctly or not, ex: click,change,keypress// TODO: check UI changing correctly or not when different props assign
/* eslint-disable redux-saga/yield-effects */import{put,call}from'redux-saga/effects';import{cloneableGenerator}from'redux-saga/utils';importfetchProductfrom'./saga';importapifrom'./api';describe('fetchProduct()',()=>{// gen = fetchProduct(); <== original styleconstgen=cloneableGenerator(fetchProduct)();it('try',()=>{constclone=gen.clone();expect(clone.next().value).toEqual(call(api.fetchProductAPI));expect(clone.next().value).toEqual(put({type: 'PRODUCTS_RECEIVED',product: 'iphone'}));});it('catch',()=>{consterror='product not found';constclone=gen.clone();clone.next();// <== before throw error, you need to execute gen.next();expect(gen.throw('product not found').value).toEqual(put({type: 'PRODUCTS_REQUEST_FAILED', error }));});});
Redux-Saga的Library (目前沒使用Library)
Redux-Saga也有Library,其中星星數最多的框架是redux-saga-test-plan (415 star),但是目前一直套用失敗,有時間與需求的話再回頭研究,有個網站分析各個Redux Saga Test Library。
it('catch',()=>{consterror='product not found';constgen=fetchProduct();gen.next();// <== before throw error, you need to execute gen.next();expect(gen.throw('product not found').value).toEqual(put({type: 'PRODUCTS_REQUEST_FAILED', error }));});
單元測試
這份文件將會告訴你哪些資訊
這份文件會告訴你如何使用jest與enzyme兩個工具來撰寫單元測試,並應用在你的react專案裡,你將會學會以下所提到的項目。
snap
檔,方便比對兩個DOM是否相同有關單元測試的前言
前端的單元測試在很多人看來都是一個可有可無的東西,理由一般有下面幾條(以下內容統一稱單元測試為單測):
其實,我大體上是同意以上觀點的。在大部分的情況下,如果公司的業務不復雜,是完全沒必要做單測的。但如果涉及到以下幾個方面,你就要考慮是否有必要引入單測了:
參考資料: React單元測試:Jest + Enzyme(一)
單元測試 - 3A原則
在撰寫單元測試的程式碼時,有個 3A 原則,來輔助設計測試程式,可以讓測試程式更好懂。3A 原則如下:
Jest - Test runner, JavaScript testing framework
Jest是一個JavaScript的測試框架,也是所謂的Test runner,類似的項目大概有Jasmine(茉莉花), Mocha(摩卡咖啡), AVA這幾種測試框架,Mocha應該是最多人使用的,但後來Facebook延續Jasmine開發Jest,也是目前專案預設的測試框架,所以就繼續沿用。
因為Jest是新框架,所以也比Mocha多了一些新功能,或是讓語法更加精鍊。
主要API使用教學
jest-cli
模組,在react-boilerplate中已經安裝在devDependencies
內,相關設定也幫你設定好了。先寫一個簡單的function來測試,接著用pass.test.js檔案來寫測試程式,完成後在cmd下執行
jest
,jest就會去所有目錄中找檔名是.test.js
或.spec.js
結尾的檔案來跑測試程式,把測試程式都放在根目錄中的__test__
資料夾也可以。不過在我們專案直接下
npm run test
就可以了,jest指令已經被整合進去了。這是我們要測試的pass.js
這是我們的測試程式pass.test.js
參考資料:React 前端單元測試教學
jest 的一些斷言方法
Jest 覆蓋率解說
stackover的網友講解如何看覆蓋率報表
Stmts(Statement):
choice12.js
程式中的第8行,一行line中就包含console.log("statement_01"); console.log("statement_02");
共2個statement。
Branch:
Funcs(Functions):
choice12.js
中只有一個Function,所以如果去測試那個function,覆蓋率就會100%。Lines:
Uncoverd Lines:
想要讓覆蓋率變成100%,只要將測試程式中的10,11,12行註解拿掉即可。
使用jest查看覆蓋率
在react-boilerplate中,使用
npm test
可以直接查看覆蓋率,一般情況則使用jest --coverage
查看,檢查的範圍可以在package.json
設定,如下如何只對單一檔案查看覆蓋率
因為無法在react-boilerplate中似乎無法對「單一」測試檔進行jest --coverage,安裝jest-single-file-coverage方能使用。
安裝 jest-single-file-coverage
在專案中的
package.json
檔案中的script
屬性加上"test:single": "node ./node_modules/jest-single-file-coverage"
就能使用
npm run test:single <file_path>
對該路徑下的測試檔案進行測試,並顯示coveragechoice12.js
choice12.test.js or chocie12.spec.js
將覆蓋率匯出成HTML報表觀看
在command line下的Uncoverd lines大概只能顯示
... 19,20,22,23
短短幾行,非常不方便,所以這邊分享觀看jest匯出的精美網頁版coverage報表。在jest中測試覆蓋率後,似乎預設會匯出HTML檔案報表(不確定是不是react-boilerplate中
已經設定好的關係),在每次測試覆蓋率後,可以在根目錄看到
coverage
資料夾,這個資料夾也在.gitingore
中被記載,直接開啟coverage/lcov-report/index.html
就能看到精美的覆蓋率報告。在index.html中可以看到每個檔案的覆蓋率
點擊index.html中的檔名則可觀看每個檔案中覆蓋率
觀看程式碼時,不同顏色與符號分別代表哪些資訊:
E
stands for 'else path not taken', which means that for the marked if/else statement, the 'if' path has been tested but not the 'else'.I
stands for 'if path not taken', which is the opposite case: the 'if' hasn't been tested.Nx
in left column is the amount of times that line has been executed.↓「
I
代表的是if-else的if沒被執行,E
代表else的部分沒被執行」示意圖以上資訊是參考類似的測試工具 Istanbul - a JavaScript test coverage tool
Snapshot功能
Snapshot testing is another new idea from Facebook. It provides an alternate way to write tests without any assertions. To write tests using assertions, Enzyme is quite useful.
快照(Snapshot)可以測試到組件的渲染結果是否符合預期,預期就是指你上一次錄入保存的結果,
toMatchSnapshot
方法會去幫你對比這次將要生成的結構與上次的區別。使用snapshot,可以防止無意間修改组件的某些部分,以及快速增加測試程式的覆蓋率,剩下像是click事件這種不太能直接用snapshot比對的部分,再靠手動去撰寫程式檢測。在這邊會使用enzyme,可以先到下一章了解enzyme
第一次執行測試的時候,只要遇到
toMatchSnapshot
,就會在測試程式的同一層建立一個__snapshots__
資料夾,並且產生snap
檔,如果原來的測試檔案叫做index.test.js
,那麼所產生的snap檔為index.test.js.snap
index.test.js.snap
快照檔內容第二次跑測試程式的時候,就會去比對snapshot,如果比對結果有出入,就會跳出失敗訊息,當你想要更新snapshot時,只要執行
jest -u
或jest --updateSnapshot
就可以了。u是update的意思。特別要注意的是,如果你Component內容改錯了,還下
jest -u
的話,就會把錯誤的DOM給記錄到snapshot裡面,所以在update之前記得要確認自己現在的程式OK才update。如過只要測試單一的
test.js
檔案,像是我如果想執行/app/components/choice
資料夾底下的測試程式,就在指令的地方下jest /app/components/choice
,就會只測試對應到的檔案,在cmd上會顯示Ran all test suites matching "/app/components/choice".
,同樣的,如果只要更新相關檔案的snap檔,就下jest /app/components/choice -u
就可以了。註解: 個人覺得snapshot只能拿來當輔助,畢竟它只能比對現在DOM架構之前有無差異,但並不知道什麼才是"真正對的"架構。
Mock(模擬) Function
以上的斷言基本是用在測試同步函數的返回值,如果所測試的函數存在異步邏輯。那麼在測試時就應該利用jest 的mock function 來進行測試。通過mock function 可以輕鬆地得到回調函數的調用次數、參數等調用信息,而不需要編寫額外的代碼去獲取相關數據,讓測試用例變得更可讀。
Enzyme - React版的
jquery
或cheerio
enzyme可以把它想像成一個react版本的jquery,使用enzyme中的shallow,就可以將react component轉換成像jquery一樣的物件,接著可以使用像是find之類的method取得所要的資訊進行比對驗證.
除了shallow以外,還有mount與render兩種method,但基本的shallow最常用。
mount
:Full Rendering,非常適用於存在於DOM API存在交互組件,或者需要測試組件完整的生命週期render
:Static Rendering,用於將React組件渲染成靜態的HTML並分析生成的HTML結構。render
返回的wrapper
與其他兩個API類似。不同的是render
使用了第三方HTML解析器和Cheerio
。簡易的Enzyme範例
如何測試有delay的Component
兩個重點:
async()
await sleep(毫秒)
以下的測試的情境是當error發生的時候,畫面下方的Snackbar會跳出並顯示錯誤訊息,2秒後自動消失。
open這屬性會從原本的false變成true,並在2秒後變回false,等待2秒這件事情我們使用
await sleep(2050)
來實作,2050是因為2000+50,如果剛好用2000的話怕會有誤差,所以多50毫秒。另外,如果程式中使用到await
,則必須把程式包在async()
內。究竟是該測些什麼
究竟Unit Test該測哪些項目呢? 以下是一些網路文章給的想法
React Component Testing with Enzyme 文章的結論
作者整理出3個他覺得該測試的項目
The Right Way to Test React Components 文章的結論
結論By Allen
Redux github 中的測試程式範例
如何測試Container - Testing Container in Provider
單純的component測試因為沒跟其他物件以及資料有相依的狀況,較容易測試,而Container則相對複雜,我們可以看到下面的
<App />
被包在很多層DOM裡面,如果缺乏這些外層DOM的資訊,將很難對<App />
中的Container進行測試,解決方式就是模擬外層DOM將這些模擬的資訊傳遞給裡面的DOM。Provider
:提供storeMuiThemeProvider
:提供Material UI的資訊LanguageProvider
:真正的Provider是IntlProvider
,提供跨國語言包ConnectedRouter
:負責記錄history解決方法就是建立一個
enzymeHelper.js
檔案,在裡面實作mountWithProviders
取代enzyme原生的mount
,在mountWithProviders
執行enzyme的mount
,並將所有要傳給Container的資訊放進context內,建立了出來的wrapper就能夠正常運作。enzymeHelper.js
enzymeHelper.js的使用方法
直接使用下方的Template開始開發Container的單元測試
Component的單元測試Template
如何測試Redux Saga
Saga的測試方式跟UI的測試方式有點不同,所以在這邊解說該如何測試,其實主要就是把原本
saga.js
中所有yield後的put
,call
等相關指令全部測過一次。範例程式
api.js
saga.js
saga.test.js
Redux-Saga的Library (目前沒使用Library)
Redux-Saga也有Library,其中星星數最多的框架是redux-saga-test-plan (415 star),但是目前一直套用失敗,有時間與需求的話再回頭研究,有個網站分析各個Redux Saga Test Library。
saga測試程式如何傳遞參數
generator.next(
value
)中的value該填入什麼才對呢?可以看到下面的saga.test.js的第3行中
generator.next(product).value
,這個product
填這邊的原因要看到saga.js,saga.js中第2行的yield put({ type: 'CALL_API' });
是接續第1行的const product = yield call(api);
,所以要將product
填在next中讓之後的測試程式可以用到product
。saga.js程式片段
saga.test.js程式片段
如何解決saga中的if-else分支問題
有時候你的 saga 可能會有不同的結果。為了要測試不同的 branch 而不重複所有流程,你可以使用 cloneableGenerator utility function,他可以複製某一步驟的generator,範例如下
如何測試 try-catch 中的 catch 事件
想了解整段程式怎麼寫的可以參考完整範例程式,其中要跳到catch的case中,就必須使用
throw()
,但在throw()
之前我們必須先執行一次next()
才行,error部分的測試程式如下。如何測試Saga程式中的flow function
基本上flow function都使用takeEvery,目前測試遇到takeEvery都會失敗,後續再研究如何測試takeEvery,或是根本不需要去測試。
常見錯誤
觸發event的方式會因應你使用
shallow
或mount
會有所不同,下面以onChange
事件為例寫下兩種不同範例。shallow
mount
取得Dialog中actions裡面的按鈕
取得Dialog內Component的方法:中的內容無法直接使用find來抓取的,所以我們用prop('children')來抓取,但隨著每個Dialog內部有著不同的
參考資料
教學影片:
文件與網站:
研究中的題目
The text was updated successfully, but these errors were encountered: