前期技術儲備
前言
我是測試小白,小小白,小小小白,最近想在成了一定規模的項目中引入測試,于是找了許些資料學習,現在已經在項目中成功引入。于是想在思路明朗和記憶深刻的時候總結下學習路徑以及寫測試中遇到的難點、坑點、注意點。給自己的近段學習成果做個總結,同時也希望能幫助到和我一樣初入測試的人。
注意注意特別注意!!!
React Native在0.56、0.57版本上測試運行有各種各樣的問題,例如:Can't run jest tests with 0.56.0、0.56 regression: jest.mock only works when defined in jestSetup.js, not in individual Snapshots tests以及筆者還沒遇到的問題,筆者親測:"Can't run jest tests with 0.56.0"這個問題在0.57中已經解決,“0.56 regression: jest.mock only works when defined in jestSetup.js, not in individual Snapshots tests”這個問題在0.57中依然存在。所以文章示例建議在0.55.4版本中運行。
初入測試一定要明白的重要概念
- 自動化測試
- 測試金字塔
- 單元/集成/e2e測試
擴展閱讀:如何自動化測試 React Native 項目 (上篇) - 核心思想與E2E自動化了解以上概念。
隨著項目越來越大,新增需求對于開發而言或許不算太大工作量,但是對于測試而言,特別是回歸測試,壓力會徒然增加很多,如果是手工測試或者是放棄一些測試用例,都是不穩定的測試。所以自動化測試的重要性就體現出來了,自動化測試的大體思路即是”測試金字塔“,測試金字塔從上到下分別是E2E測試、集成測試、單元測試。E2E測試是需要真實編譯打包在模擬器上或者真機上模擬用戶行為走測試流程,測試結果受網絡,彈窗,電話等不可控影響較大,因此不能過于信任,因此E2E測試出的Bug最好能到集成測試中重現,集成測試中的出現的Bug最好能在單元測試中重現,若不能重現則應該加入更多的單元/集成測試來重現Bug。集成和單元測試都不需要編譯打包運行,因此它們的執行速度非常快,所以項目中測試代碼量應該是單元測試大于集成測試,集成測試大于E2E測試,從而形成自動化測試金字塔。
- Snapshot
- Mock
- JavaScript Testing utility:例如Detox、Enzyme
- JavaScript Test runners and assertion libraries:例如Jest
文章后面會重點解釋以上概念。
React Native對于測試的支持
- ReactNative官方測試介紹: facebook.github.io/react-nativ…
If you're interested in testing a React Native app, check out the?React Native Tutorial?on the Jest website.
- jestjs.io/docs/en/tut…
Starting from react-native version 0.38, a Jest setup is included by default when running
react-native init
.
通過React Native
和Jest
官方描述,可以得到結論:在react-native 0.38及后續版本在react-native init
時已經默認植入了Jest測試庫,所以我們可以0配置開始嘗試編寫測試代碼。
使用以下方式開始嘗試一下吧 (*^^*) 創建ios
和android
同級目錄下創建__test__
文件夾,在__test__
文件夾下創建helloworld.test.js
文件,并輸入以下代碼:
it('test',()=>{expect(42).toEqual(42)
})
復制代碼
在終端執行:npm test
查看測試結果。 入門是不是超簡單o(* ̄ ̄*)o!
注:不是一定要在
ios
和android
同級的目錄創建__test__
文件夾才能寫測試代碼,項目下的*.test.js
都可以執行測試。
Jest必備知識
- Expect
- Snapshot Testing
- Mock Functions
請閱讀 jestjs.io/docs/en/get… 的 Introduction 章節的前5篇文章(到Mock Function為止),Guides章節的第一篇文章。
Jest 是一個運行測試和斷言的庫(Test Runner and assertion libraries),Jest通過Expect來斷言當前結果和預期結果是否相同,這些結果是這里所涉及到的數據類型。Jest使用Mock來模擬一些Function、Module以及Class來方便測試(Mock測試中不需要真實去執行的代碼,例如Fetch,Platform.OS等)。
Snapshot翻譯成中文是快照的意思,以前的UI測試是執行測試腳本并在停留的頁面上截圖,當再次執行相同的測試腳本時會拿前后的截圖做對比,如果像素相同則測試通過,像素不相同則測試不通過。在Jest中對React的UI測試可以通過Snapshot生成序列化結構樹(文本形式),對比前后生成的結構樹即可。Snapshot不僅僅可以用來測試UI,它可以用來測試任何可以序列化的結構,例如Action、Store等,在文章后面會有所提及。
前期技術儲備好了我們就可以開始著手寫測試了^_^
單元測試
Redux 邏輯測試
官方推薦閱讀:Testing React Native with the new Jest?—?Part II
Redux中的Reducer測試
Reducer是純函數,也就是說在有相同的輸入值時,就一定是相同的輸出,因此是很容易測試的。
it('start upload action will combine upload\'s watting queue and failed queue then update upload\'s uploading state', () => {let currentState = Map({'uploadTestKey': new Upload({name: 'uploadTestKey',wattingQueue: List([new UploadItem({name: 'fileTwo',filepath: 'fileTwoPath'})]),uploadedQueue: List([new UploadItem({name: 'fileThree',filepath: 'fileThreePath'}),]),failedQueue: List([new UploadItem({name: 'fileOne',filepath: 'fileOnePath'}),]),})})currentState = UploadReducer(currentState, UPloadActions.startUpload({upload: 'uploadTestKey'}))expect(currentState).toMatchSnapshot()
})
復制代碼
上面的代碼示例是測試UploadReducer
對固定輸入currentState
和UPloadActions.startUpload({upload: 'uploadTestKey'})
的輸出是否正確,這里需注意以下兩點:
1、要確保第一次運行npm run test
后產生的__snapshots__/<測試文件名稱>.snap
里面內容的正確性。因為expect(currentState).toMatchSnapshot()
與expect(value).toEqual(someValue)
的寫法不同,后一種可以在寫測試用例時直接給出期望值,前一種是測試用例運行完自動將期望值寫入到了__snapshots__/<測試文件名稱>.snap
文件中,因此在第一次運行完測試用例我們需要確認生成的snapshot
的正確性。toMatchSnapshot()
的好處是不需要copy代碼在測試用例中,如果不使用toMatchSnapshot()
,我們的測試用例將寫成以下形式:
it('start upload action will combine upload\'s watting queue and failed queue then update upload\'s uploading state', () => {let currentState = Map({'uploadTestKey': new Upload({name: 'uploadTestKey',wattingQueue: List([new UploadItem({name: 'fileTwo',filepath: 'fileTwoPath'})]),uploadedQueue: List([new UploadItem({name: 'fileThree',filepath: 'fileThreePath'}),]),failedQueue: List([new UploadItem({name: 'fileOne',filepath: 'fileOnePath'}),]),})})currentState = UploadReducer(currentState, UPloadActions.startUpload({upload: 'uploadTestKey'}))expect(currentState.is(Map({'uploadTestKey': new Upload({name: 'uploadTestKey',wattingQueue: List([new UploadItem({name: 'fileTwo',filepath: 'fileTwoPath'}),new UploadItem({name: 'fileOne',filepath: 'fileOnePath'}),]),uploadedQueue: List([new UploadItem({name: 'fileThree',filepath: 'fileThreePath'}),]),failedQueue: List([]),})}))).toBe(true)
})
復制代碼
這樣就造成了代碼冗余,這時snapshot
的重要性就提現出來了。
2、既然是單元測試,那我們寫的每個測試用例的職責都要單一,不要在單元測試中寫出集成測試出來,這是剛學測試經常難以區分的。測試的語法并不難,難得是寫出什么樣的測試用例。例如以上的測試用例是測試一個上傳隊列組件,它的reducer
可以處理多個action
,例如push
、delete
、upload
等,那我們應該怎樣為這個reducer
寫單元測試呢?筆者一開始就跑偏了,寫出了這樣的測試用例,各位看官可以看看:
describe("upload component reducer test", () => {describe("one file upload", () => {let currentState = Map({})beforeAll(() => {currentState = UploadReducer(currentState, UPloadActions.registerUpload({upload: 'uploadTestKey'}))expect(currentState).toMatchSnapshot()})afterAll(() => {currentState = UploadReducer(currentState, UPloadActions.destroyUpload({upload: 'uploadTestKey'}))expect(currentState).toMatchSnapshot()})...test("handle upload success", () => {let state = UploadReducer(currentState, UPloadActions.pushUploadItem({upload: 'uploadTestKey', name: 'fileOne', filePath: 'fileOnePath'}))expect(state).toMatchSnapshot()state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))expect(state).toMatchSnapshot()state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))expect(state).toMatchSnapshot()state = UploadReducer(state, UPloadActions.uploadItemSuccess({upload: 'uploadTestKey', id: '12345'}))expect(state).toMatchSnapshot()state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))expect(state).toMatchSnapshot()})test("handler upload failed", () => {...})test("handler reupload success", () => {let state = UploadReducer(currentState, UPloadActions.pushUploadItem({upload: 'uploadTestKey', name: 'fileOne', filePath: 'fileOnePath'}))state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))state = UploadReducer(state, UPloadActions.uploadItemFailed({upload: 'uploadTestKey'}))state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))expect(state).toMatchSnapshot()state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))expect(state).toMatchSnapshot()state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))state = UploadReducer(state, UPloadActions.uploadItemSuccess({upload: 'uploadTestKey', id: '12345'}))state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))expect(state).toMatchSnapshot()})})describe("mult file upload", () => {let currentState = Map({})beforeAll(() => {...})afterAll(() => {...})...test("handle upload successed", () => {...})test("handle upload failed", () => {...})test("hanlde reupload successed", () => {...})})
})
復制代碼
可以看上以上單元測試的問題嗎?在這里引入這篇文章所舉的例子:
reducer
增加了新的action
處理,那測試文件中應該添加多少個測試用例呢? 于是筆者改成了以下寫法: describe("upload component reducer test", () => {it('register upload action will register a upload queue to state', () => {let currentState = Map({})currentState = UploadReducer(currentState, UPloadActions.registerUpload({upload: 'uploadTestKey'}))expect(currentState).toMatchSnapshot()})it('destroy upload action will remove upload queue from state', () => {let currentState = Map({'uploadTestKey': new Upload({name: 'uploadTestKey'})})currentState = UploadReducer(currentState, UPloadActions.destroyUpload({upload: 'uploadTestKey'}))expect(currentState).toMatchSnapshot()})it('push upload item action will add an uploadItem into upload\'s wattingQueue', () => {...})it('delete upload item action will remove an uploadItem from upload\'s all queue', () => {...})...
})
復制代碼
reducer
能處理多少個action
就有多少個測試用例,是不是明了多了? 示例代碼
Redux中的Action Creator測試
與Reducer
同樣的道理,也是要注意兩點,一個是測試用例的職責要對,一定要記住它是“單元測試”,我們只需要保證單個Action creator
有特定的輸入就有特定的輸出,而且要對第一次運行測試用例的輸出snapshot
進行檢查,保證期望值的正確性。 示例代碼
如何測試異步Action
通常的Action
是一個Object
對象,帶有type
屬性即可,但是異步Action它返回的不是一個Object
而是一個特殊的Function
,需要類似于redux-thunk
的中間件來處理。因此我們在測異步Action時需要Mock兩個模塊,一個是網絡異步所需要的fetch
,另一個就是可以派發Async Action
的Store
。
請先閱讀Jest官方的Mock相關文檔:Mock Functions、manual-mocks
Mock fetch可以使用庫:jest-fetch-mock Mock store可以使用庫:redux-mock-store 具體配置查看官方README,這是配置好的項目。 Object
類型的Action
測試寫法:
it('register upload action' , () => {store.dispatch(UploadActions.registerUpload({upload: 'uploadKey'}))expect(store.getActions()).toMatchSnapshot()
})
復制代碼
異步Action測試寫法:
it('upload one file fail action test', () => {fetch.mockResponseOnce(JSON.stringify({ error: new Error('fail') }))return store.dispatch(UploadActions.upload('uploadKey', config)).then(() => {expect(store.getActions()).toMatchSnapshot()})
})
復制代碼
異步測試有多種寫法,分別用來處理callBack
、Promise
、async/await
,具體請查閱官方文檔。
Component測試
上面詳細講述了關于Redux的單元測試,下面來看看Component如何做單元測試。
請先閱讀Testing React Native with the new Jest?—?Part I
需要注意的是,網上有許多文章在寫組件測試的時候都使用了react-native-mock,用來mock RN的庫,但是在RN0.37版本開始,內置于react-native的Jest設置自帶一些應用于react-native庫的mock。可以在setup.js中查閱,因此不需要再引入react-native-mock。
Component
測試的核心點:
- 給不同的props會有不同的
Dom
輸出。 - 使用主動執行實例方法來模擬
State
的變化輸出不同的Dom
。 - 測試使用
connect(component)
包裹的組件時,mockconnect
組件連接的props
,直接測試被connect
包裹的組件。 - 測試使用
HOC
的組件時,分別測試ComponentWrap
和Component
。
注意上面列表加粗的文字,這些文字就是我們寫Component
測試的著手點。
UI Render測試,我們測試的是不同的props
有不同的Dom
:
it('render login screen with init state', () => {const loginWrap = shallow(<LoginScreenhandleSubmit={handleSubmit}valid={false}submitting={false}/>)expect(toJson(loginWrap)).toMatchSnapshot()
})
復制代碼
在上段的代碼中,我們可以改變valid
這些屬性值,然后使用toMatchSnapshot
來保留snap。這里涉及的庫有:enzyme
,enzyme-to-json
,知識點有:shallow
。
enzyme是使用javascript
語言為react
寫的測試工具,可以用來快速的獲取Component
的輸出(Dom
),操控Dom
,以及對Dom
寫各種斷言。類似的有React Test Utilities和react-testing-library,React Test Utilities是React官方出的測試工具,也可以輸出Dom
,但是它不能操作Dom
,沒有提供Selector
。react-testing-library與enzyme的功能很接近,但是不支持react-native
,支持react
。
enzyme-to-json可以將shallow
的結果json
化輸出,一般配合Jest
的toMatchSnapshot
使用。 Shallow
的render方式是淺渲染,只生成Dom樹的一層,例如:
//ComponentA.js
import React from 'react'
import {Text,View,
} from 'react-native'class ComponentA extends React.Component {render() {return (<View><ComponentB /></View>)}
}
class ComponentB extends React.Component {render() {return (<Text>Hello world</Text>)}
}export default ComponentA
復制代碼
//ComponentA.test.js
import ComponentA from './ComponentA'
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'it('shallow ComponentA', () => {const wrap = shallow(<ComponentA/>)expect(toJson(wrap)).toMatchSnapshot()
})
復制代碼
//ComponentA.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`shallow ComponentA 1`] = `
<Component><ComponentB />
</Component>
`;
復制代碼
使用Shallow
的渲染結果就是<View><ComponentB/></View>
,它不會再把ComponentB
展開獲得<View><Text>Hello world</Text></View>
這種結果。這樣我們就不用關心子組件的行為,我們之要專心測ComponentA
即可。
enzyme
和enzyme-to-json
的安裝,參考官網:airbnb.io/enzyme/
UI交互測試,我們需要主動調用實例方法來觸發state
的更改:
//Foo.js
import React from 'react'
import {Switch
} from 'react-native'export default class extends React.Component {constructor() {super(...arguments)this.state = {value: false}}_onChange = (value) => {this.setState({value: value})}render() {return (<Switch onValueChange={this._onChange} value={this.state.value}/>)}
}
復制代碼
//Foo.test.js
import Foo from './Foo'import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'it('Foo change state', () => {const wrap = shallow(<Foo/>)expect(wrap.state(['value'])).toEqual(false)expect(toJson(wrap)).toMatchSnapshot()const firstWrap = wrap.first()firstWrap.props().onValueChange(true)expect(wrap.state(['value'])).toEqual(true)expect(toJson(wrap)).toMatchSnapshot()
})
復制代碼
//Foo.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`Foo change state 1`] = `
<Switchdisabled={false}onValueChange={[Function]}value={false}
/>
`;exports[`Foo change state 2`] = `
<Switchdisabled={false}onValueChange={[Function]}value={true}
/>
`;
復制代碼
在這個例子中,在firstWrap.props().onValueChange(true)
前分別打印了snap
,并且斷言state.value
的值,來測試onValueChange
引起的state
的更改。firstWrap.props().onValueChange(true)
就是主動調用實例方法的行為。
HOC測試:
在以上的兩個例子中,可以掌握常規組件的單元測試,那么Hoc組件如何測試呢?其實實現方式也很簡單,我們把HOC
拆開來看,可以分別測Higher Order
和Component
,Component
的測試和上兩個例子一樣,需要注意的是,要分別導出Higher Order
和Component
以及HOC
:
//Hoc.js
import React from 'react'
import {View
} from 'react-native'export function fetchAble(WrappedComponent) {return class extends React.Component{_fetchData = () => {console.log('start fetch')}render() {return (<WrappedComponent fetchData={this._fetchData}/>)}}
}export class Com extends React.Component {render() {return (<ComponentA/>)}
}export default fetchAble(View)
復制代碼
//Hoc.test.js
import {fetchAble} from './Hoc'
it('Hoc test', () => {const A = (props) => <View/>const B = fetchAble(A)const fetchWarp = shallow(<B/>)const wrapA = fetchWarp.find(A)expect(wrapA).not.toBeUndefined()expect(wrapA.props().fetchData).not.toBeUndefined()wrapA.props().fetchData()expect(console.log.mock.calls.length).toEqual(1)expect(console.log.mock.calls[0][0]).toEqual('start fetch')
})
復制代碼
在
setupJest
中配置了mockconsole
。
Redux Connect與HOC是同樣的道理
組件測試的參考文章(搭梯子):
Sharing and Testing Code in React with Higher Order Components
Testing React Component’s State
Unit Testing Redux Connected Components
這一篇主要是圍繞組件和Redux寫單元測試,下一篇將開始寫集成以及e2e測試
歡迎關注我的簡書主頁:www.jianshu.com/u/b92ab7b3a… 文章同步更新^_^