開始測試React Native App(下篇)

lyxia_iOS發表於2018-09-28

前言:


開始測試React Native App(上篇)中編寫了redux-upload-queue針對ReducerAction Creator的單元測試,測試程式碼可以在這裡查閱。這篇文章基於開始測試React Native App(上篇)繼續完成整合測試以及E2E測試。

整合測試

Action Creator的測試中,引入了redux-mock-store庫,按官方的話來說,這個庫只是用來測試Redux async action creatorsmiddleware,它不是用來測試reducer相關的邏輯,換句話來說它不會更新Redux Store,所以如果你想把reduceraction結合在一起測試建議使用redux-actions-assertions。 筆者在剛學測試時沒有認真看文件這段話,導致寫出瞭如下程式碼:

const rootReducer = combineReducers({
    upload: UploadReducer
})
let initState = {}
export const store = mockStore((actions) => {
    let currentState = initState
    actions.forEach(action => {
        currentState = rootReducer(currentState, action)
    });
    return currentState
})
複製程式碼

變相的使用redux-mock-store實現了結合reduceraction的測試,能更改Redux Store,在效能上肯定是不優的,每次獲取State都要遍歷所有派發的actionreducer。所以還是建議使用redux-actions-assertions,在該篇文章中採用的是不優的解決方案。

在解決了以上測試的技術點後,就可以開始寫組合reduceraction在一起的整合測試了:

import * as UploadActions from '../UploadActions'
import config, {store} from './UploadConfig'

afterEach(() => {
  store.clearActions()
  fetch.resetMocks()
})

...
//使用reducer和action模擬多張圖片部分上傳失敗,重新上傳成功的整合測試
it('upload mult fail and reupload action test', () => {
  fetch.mockResponses(
    [
      JSON.stringify({ error: null, id: '123456' })
    ],
    [
      JSON.stringify({ error: null, id: '123456' })
    ],
    [
      JSON.stringify({ error: new Error('fail') })
    ],
    [
      JSON.stringify({ error: null, id: '123456' })
    ],
  )

  store.dispatch(UploadActions.registerUpload({upload: 'uploadKey'}))
  store.dispatch(UploadActions.pushUploadItem({upload: 'uploadKey', name: 'fileOne', filePath: 'filePathOne'}))
  store.dispatch(UploadActions.pushUploadItem({upload: 'uploadKey', name: 'fileTwo', filePath: 'filePathTwo'}))
  store.dispatch(UploadActions.pushUploadItem({upload: 'uploadKey', name: 'fileThree', filePath: 'filePathThree'}))
  return store.dispatch(UploadActions.upload('uploadKey', config))
          .then(() => {
            return store.dispatch(UploadActions.upload('uploadKey', config))
          })
          .then(() => {
            expect(store.getActions()).toMatchSnapshot()
            expect(store.getState()).toMatchSnapshot()
          })
})
複製程式碼

上面的測試程式碼首先派發出注冊上傳佇列的動作(UploadActions.registerUpload),然後依次派發出在註冊的上傳佇列中新增上傳項的動作(UploadActions.pushUploadItem),再派發非同步上傳動作(UploadActions.upload)開始上傳,因為使用fetch.mockResponsesmock了多次網路請求的返回結果來模擬上傳的結果,所以模擬出了第一次上傳時第三個檔案(fileThree)上傳失敗,失敗後再次派發上傳動作UploadActions.upload返回成功,判斷整個流程走完後store.getActions()store.getState()是否符合預期。

這個測試用例涉及到了派發action,使用reducer處理action,以及更改Store的狀態,所以它是一個整合測試,也是單元測試的組合測試。

示例程式碼

其實整合測試更加的符合初學者對測試的直觀想法,比如當我說我要測試上傳元件的redux邏輯是否有問題時,自然而然就會想到要派發一系列action,再看reducer是否能正常的處理這些actionStore結果是否符合預期。在redux-upload-queue這個元件中,不但實現了Redux處理上傳佇列的整套邏輯,還使用HOC的方式,讓任意元件可以快速的整合上傳佇列功能,例如:

...
import {redux_upload} from 'redux-upload-queue'

class Foo extends Component {
  componentDidMount() {
    this.props.pushUploadItem('fileOnePath', 'fileOne')
    this.props.startUpload()
  }
  render() {return <View/>}
}
...
export default redux_upload({ upload: 'uploadKey', config: config })(Foo)
複製程式碼

上面的示例中對元件Foo快速的整合了上傳佇列的功能,那麼redux_upload是否能正確的讓被包裹的元件有上傳功能呢?我們可以寫以下整合測試用例來證明:

import config, {store, Foo} from './UploadConfig'
import uploadComponent from '../UploadComponent'
...
test('uploadComponent new', () => {
    fetch.mockResponseOnce(JSON.stringify({ error: null, id: '123456' }))
    
    const Component = uploadComponent({ upload: 'uploadKey', config: config })(Foo)
    const componentWrap = shallow(
        <Component store={store}/>
    )
    const fooWrap = componentWrap.shallow()
    const fooProps = fooWrap.props()
    fooProps.pushUploadItem('fileOnePath', 'fileOne')
    return fooProps.startUpload().then(() => {
        expect(store.getActions()).toMatchSnapshot()
    })
})
複製程式碼

通過shallow來模擬元件裝載,然後使用ShallowWrapperprops()來獲取被裝載的Foo元件的所有屬性,呼叫屬性的pushUploadItemstartUpload方法來觸發上傳操作,預期會觸發與之前測試上傳佇列Redux邏輯差不多的Actions,都是先註冊佇列,新增上傳項然後開始上傳等,只是註冊的唯一識別符號、新增上傳項的物件以及數量、上傳的結果不同。

注意<Component store={store}/>這一行程式碼,store={store}是用來給Redux connect提供Store的,相當於使用react-redux中的Provider:<Provider store={store}><Component/></Provider>示例程式碼

E2E測試


E2E測試就是編譯安裝App到模擬器或真機,在App中模擬使用者的行為進行測試,一般用來測試App的主要流程(不是全部流程),因為E2E測試受周邊環境影響較大(網路等因素),因此測試結果不完全可靠。

Detox

Detox是一個移動App自動化E2E灰盒測試框架,是第一個支援React Native專案的E2E測試框架,可以結合Jest使用,安裝與配置也是很快的。

Detox設計原則中我們可以瞭解到它是一個灰盒測試框架,這種測試框架是從App的內部操控測試(通過testId等方式查詢到UI元素,然後執行TapActions來觸發各種手勢輸入等(模擬使用者),最後通過isVisibleMatcher來判斷期望值),保證App的核心流程正確。它依賴Native端的灰盒測試框架:EarlGrey for iOSEspresso for Android,使用基於JSON的反射機制,讓JavaScript直接呼叫Native測試框架的方法,在JavaScript端提供了一系列易於使用API,完全的抽象了Native引擎下發生的複雜呼叫邏輯,因此它寫出來的測試可讀性高。 測試指令碼與被測試的App的通訊原理:

image.png

依賴websockets讓執行在nodejs的測試指令碼和執行在裝置上的App通訊,實現了真實的雙工通訊,相比與其他類似REST的協議要更快更靈活,執行在nodejs端的測試讓它能在多個平臺上執行。

測試與App同步

這種E2E測試指令碼執行App流程,讓人最困惑的的就是它是如何將測試與App同步,App複雜的操作(例如訪問伺服器資料或執行動畫)經常需要大量的時間去完成,在這些操作完成之前我們不能繼續執行測試程式碼,否則會使測試失敗(例如正在測試登入流程,在沒登入成功時你就斷言進入首頁,測試就會失敗),那我們如何將測試與App同步?

經常會想到的解決方案是手動執行sleep()來同步,但是在不同的裝置上,不同的網路狀態下,執行相同的操作所花費的時間不同,手動sleep()要不會造成不必要的時間浪費,讓測試變慢,要不時間不給充足會讓測試直接失敗。

Detox的同步方式是:自動同步,這種同步方式就像魔術一樣,你在寫了一行測試程式碼後寫下一行測試程式碼無需關心中間的時間間隔問題(資料是不是還沒有獲取到,轉場動畫是否還沒執行完等),Detox會等待App穩定之後才會去執行下一行程式碼。例如有一個已經發出的網路請求,那麼直到網路請求完成測試才會執行下一行程式碼。

這種自動同步要百分之百的正確是非常困難的,經常會有一些異常情況,Detox正在對這些異常情況進行優化,因此大部分情況下都需要考慮同步問題。那如果遇到了同步問題應該怎麼辦呢?這裡給出了具體的解決方案,包括:

  • 不能自動同步的原因。
  • 可以自動同步的場景。
  • 手動切換到非自動同步模式,然後使用waitFor做手動同步。
  • 使用react-native-repackager重寫e2e下執行的程式碼,就像*.ios.js*.android.js一樣,可以通過*.e2e.js來載入在E2E環境測試時執行的程式碼。

注意: 1、同步狀態難以解救時要去看下一自己的程式碼是不是使用setTimeout等不當操作造成了濫用資源,記憶體洩漏。 2、react-native-repackager在0.55.*之後的版本因為這個PR而不再需要,但依然是通過定義E2E的flavor來使用特定的*.e2e.js自定義副檔名。

Mock

之前提到過,E2E測試受環境因素影響大,例如網路狀態,模擬器中沒有圖片庫,沒有聯絡人等,想象一下如果我們能夠在執行E2E測試時達到以下需求:

  • 使用本地的Mock HTTP Server來代替生產環境中真實的伺服器訪問(這裡推薦我的美女同事寫的兩篇文章:前後端分離——資料mockjson-server 接入專案說明用來Mock伺服器資料)。
  • 當執行在模擬器時,不去訪問裝置上的聯絡人,而是返回Mock的聯絡人。

在對真實專案E2E時,諸如以上的場景還有許多,因此可以使用build flavouring來自定義file extensions,然後編寫Mock。這樣可以大量減少受E2E執行結果受環境因素的影響,具體可以看react-native-repackager Better support for custom file extensions (and build flavours)

Artifacts

最後一個我特別喜歡的功能點,那就是Artifacts了,它可以在測試過程中以多種方式記錄下測試過程,例如錄影、截圖、log等。大家有興趣可以看文件,簡單實用。

總結選擇Detox進行E2E測試的原因:

  • 程式碼跨平臺,因為它是在nodejs中執行的。
  • 可以在真機、模擬器中執行App。
  • 結合Jest使用時,易配置,上手難度小。
  • 使用async/await自動同步測試和App的狀態,大部分情況下無需寫waitFor
  • 使用Artifacts可以在測試過程中錄製視訊和截圖。
  • 提供全面的Actions,例如點選(單點,多點,長按)、滑動(上下左右四個方向以及速度位置的控制)、輸入文字、滾動等。
  • 提供Matcher獲取UI元素,可以通過testID、文字內容、nativeViewType來定位UI,即可以查詢到js端定義的UI,可以查詢到native端定義的UI。
  • 提供Expect來斷言期望值,可以斷言UI元素是否存在,是否可見。

還有更多好用的API可以在文件中查閱。

可執行官方示例體驗:github.com/wix/detox/t…

完(初)


之所有寫【完(初)】,是因為在這裡測試的初級學習就結束了,其實在學習測試的過程中Mock是一個很重要的概念,幾乎無處不在Mock,想必大家在文章中也看到了我用的Mock庫:

這些Mock不限於Jest中的Mock,還有在編寫開發程式碼中的Mock,除了使用這些Mock庫,在實際專案中寫測試時也需要自己寫大量的對第三方庫或者對自己寫的模組的Mock,以及React Native雖然自帶Mock但是還有一些模組(Platform等)沒有被Mock到,也要自己Mock。除了模組的Mock還有Function的Mock,資料的Mock,Global的Mock等等,具體參考Testing React Native Apps

Test Runner:

  • Jest:JavaScript測試框架

Test Utility:

歡迎關注我的簡書主頁:www.jianshu.com/u/b92ab7b3a… 文章同步更新^_^

相關文章