前期技術儲備
前言
我是測試小白,小小白,小小小白,最近想在成了一定規模的專案中引入測試,於是找了許些資料學習,現在已經在專案中成功引入。於是想在思路明朗和記憶深刻的時候總結下學習路徑以及寫測試中遇到的難點、坑點、注意點。給自己的近段學習成果做個總結,同時也希望能幫助到和我一樣初入測試的人。
注意注意特別注意!!!
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.
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 邏輯測試
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如何做單元測試。
需要注意的是,網上有許多文章在寫元件測試的時候都使用了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
中配置了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… 文章同步更新^_^