開始測試React Native App(上篇)
前言
我是測試小白,小小白,小小小白,最近想在成了一定規模的專案中引入測試,於是找了許些資料學習,現在已經在專案中成功引入。於是想在思路明朗和記憶深刻的時候總結下學習路徑以及寫測試中遇到的難點、坑點、注意點。給自己的近段學習成果做個總結,同時也希望能幫助到和我一樣初入測試的人。
注意注意特別注意!!!
React Native在0.56、0.57版本上測試執行有各種各樣的問題,例如: ofollow,noindex">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.
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必備知識
請閱讀 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( <LoginScreen handleSubmit={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/fbAQLP exports[`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/fbAQLP exports[`Foo change state 1`] = ` <Switch disabled={false} onValueChange={[Function]} value={false} /> `; exports[`Foo change state 2`] = ` <Switch disabled={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
中配置了mock console
。
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… 文章同步更新^_^