構建 F8 2016 App 第四部分:測試
上一篇:構建 F8 2016 App 第三部分:React Native的數據交互
這是為了介紹 React Native 和它的開源生態的一個系列教程,我們將以構建 F8 2016 開發者大會官方應用的 iOS 和 Android 版為主題。
在傳統軟件開發生命周期里,測試環節通常往往僅僅是作為一個臨近開發結束時才開始進行的特殊環節。由于新推出開源框架發布時往往并未有任何相關測試技術支持,當使用這些框架進行開發時,這種說法便更接近事實真相。
幸運的是, 非死book 在 React Native 構建之初很早的就考慮到了測試策略。在這部分我們將介紹編碼階段如何使用 Nuclide ,Flow , 以及 Jest 改善 React Native 代碼質量。
Flow : 利用類型檢查避免編寫糟糕代碼
Flow為JavaScript 提供以漸進方式工作的靜態類型檢查,允許我們將 Flow 特性逐步添加到代碼中。這是個非常有用的設計。當我們僅僅想為部分代碼引入類型檢查時, 我們不必為兼容 Flow 而去重寫整個 app 。
我們決定從一開始就完全采用 Flow 來配合 React Native 搭建 F8app,在一切必要的地方添加類型注解(type annotations ) ,讓 Flow 能在整個代碼開發過程中一直盡情發揮力量。
下面我們以曾經在數據教程部分曾提及的一個簡單 action 作為示例:
/* from js/actions/login.js */
/*
* @flow
*/
...
function skipLogin(): Action {
return {
type: 'SKIPPED_LOGIN',
};
}
我們在文件頂部添加 @Flow 標簽(通知 Flow 需檢查此段代碼)。我們使用 Flow 的類型注解 限定 skipLogin() 返回值類型必須是 type Action ,由于該Action類型并未在 React Native 及 Redux 里內置,因此在這里我們需要自己對該類型進行定義:
export type Action =
{ type: 'LOADED_ABOUT', list: Array<ParseObject> }
| { type: 'LOADED_NOTIFICATIONS', list: Array<ParseObject> }
| { type: 'LOADED_MAPS', list: Array<ParseObject> }
| { type: 'LOADED_FRIENDS_SCHEDULES', list: Array<{ id: string; name: string; schedule: {[key: string]: boolean}; }> }
| { type: 'LOADED_CONFIG', config: ParseObject }
| { type: 'LOADED_SESSIONS', list: Array<ParseObject> }
| { type: 'LOADED_SURVEYS', list: Array<Object> }
| { type: 'SUBMITTED_SURVEY_ANSWERS', id: string; }
| { type: 'LOGGED_IN', data: { id: string; name: string; sharedSchedule: ?boolean; } }
| { type: 'RESTORED_SCHEDULE', list: Array<ParseObject> }
| { type: 'SKIPPED_LOGIN' }
| { type: 'LOGGED_OUT' }
| { type: 'SESSION_ADDED', id: string }
| { type: 'SESSION_REMOVED', id: string }
| { type: 'SET_SHARING', enabled: boolean }
| { type: 'APPLY_TOPICS_FILTER', topics: {[key: string]: boolean} }
| { type: 'CLEAR_FILTER' }
| { type: 'SWITCH_DAY', day: 1 | 2 }
| { type: 'SWITCH_TAB', tab: 'schedule' | 'my-schedule' | 'map' | 'notifications' | 'info' }
| { type: 'TURNED_ON_PUSH_NOTIFICATIONS' }
| { type: 'REGISTERED_PUSH_NOTIFICATIONS' }
| { type: 'SKIPPED_PUSH_NOTIFICATIONS' }
| { type: 'RECEIVED_PUSH_NOTIFICATION', notification: Object }
| { type: 'SEEN_ALL_NOTIFICATIONS' }
| { type: 'RESET_NUXES' }
;
這里我們創建了 Flow 類型別名( Flow type alias ),限定了 type Action 的樣式范圍。比如,SKIPPED_LOGIN Action 僅能包含一個自己的type標簽, LOADED_SURVEYS Action 則會返回 type 標簽以及一個 list 列表。相關 Action Creator 如下:
/* from js/actions/surveys.js */
async function loadSurveys(): Promise<Action> {
const list = await Parse.Cloud.run('surveys');
return {
type: 'LOADED_SURVEYS',
list,
};
}
我們在 app 里使用了大量不同 Action ,強類型檢查除了會幫助我們發現類似 type 標簽拼寫錯誤這樣的低級錯誤,還會發現如數據格式錯誤這樣的比較重要的問題。
我們在 Reducers 也進行了一樣的強類型檢查:mp
/* from js/reducers/surveys.js */
function surveys(state: State = [], action: Action): State {
if (action.type === 'LOADED_SURVEYS') {
return action.list;
}
...
return state;
}
由于 action 參數被指定為與前面一樣的 Action 類型,因此 Reducer 函數必須使用一個有效的 action.type 。我們通過類型別名為 Reducer state 樹選項定義樣式:
/* from js/reducers/user.js */
export type State = {
isLoggedIn: boolean;
hasSkippedLogin: boolean;
sharedSchedule: ?boolean;
id: ?;
name: ?string;
};
const initialState = {
isLoggedIn: false,
hasSkippedLogin: false,
sharedSchedule: null,
id: null,
name: null,
};
function user(state: State = initialState, action: Action): State {
...
}
我們在數據教程部分曾為你展示過 initialState ,現在你會看到,我們是怎樣保證 state 樹選項與 Flow 定義的類型相一致。當 Reducer 發送或者嘗試返回任何不符合定義樣式的 state 時,都會發生 Flow 類型檢查錯誤。
請注意, Folw 檢查僅在編譯階段運行, Recat Native packger 則會自動去除-這意味著在代碼中使用 Flow 不會造成任何執行性能損失。
當然,目前每次想對某些代碼進行測試時,我們仍然需要手動運行 Flow 類型檢查(通過 Flow 命令行接口)。不過我們可以通過 Nuclide 在編碼時進行這樣的查驗。
Nuclide:React Native 開發環境
Nuclide 網站上有為 React Native 提供的定制功能的全面介紹。我必須得說, Neculite 是專為 React Native 的 非死book 研發團隊以及 非死book app 專業研發人員而開發的頂級 React Native IDE 。
Nuclite 對 Flow 的集成格外讓我們感興趣。在這里讓我們看看 user Reducer 的一段代碼:
if (action.type === 'SKIPPED_LOGIN') {
return {
isLoggedIn: false,
hasSkippedLogin: true,
sharedSchedule: null,
id: null,
name: null,
};
}
前面有提到,我們為 Reducer 函數定義了返回值樣式。在 Nuclide ,我們可以實時看到錯誤發生:
Your browser does not support the HTML5 video tag.
若我們有遺漏了 State 類型的任何部分,我們會瞬間收到內容為未返回正確對象類型的反饋。這種概率事件在快速構建 app 時會頻繁發生。
Nuclite 對所有相關 Flow 類型檢查都是如此。這意味著在代碼還在編寫時,類型錯誤便能暴露出來,同時我們還能對相關問題進行修正。而這一切無需再等到到開發接近完成時才進行。
這或許并不直觀,但確實提高了開發速度。要想在代碼中發現遺漏真的是件很困難的事。而在 app 接近開發完成時,這會更加棘手。
Jest : 對可能造成 BUG 的改動進行單元測試
jest 是一個面向 JavaScipt 的單元測試框架,同時它在 React Native app下也表現不俗。
我們h會用這些單元測試(也被稱作回歸測試)來確保對已經開發完成的結構化代碼的改動并未引入 bug 。
作為例子,下面我們準備通過一個 Jest 測試來保證 Reducer 能夠繼續按預期處理地圖數據:
jest.autoMockOff();
const Parse = require('parse');
const maps = require('../maps');
describe('maps reducer', () => {
it('is empty by default', () => {
expect(maps(undefined, {})).toEqual([]);
});
it('populates maps from server', () => {
let list = [
new Parse.Object({mapTitle: 'Day 1', mapImage: new Parse.File('1.png')}),
new Parse.Object({mapTitle: 'Day 2', mapImage: new Parse.File('2.png')}),
];
expect(
maps([], {type: 'LOADED_MAPS', list})
).toEqual([{
id: jasmine.any(String),
title: 'Day 1',
url: '1.png',
}, {
id: jasmine.any(String),
title: 'Day 2',
url: '2.png',
}]);
});
});
Jest 本身非常易于閱讀(注意:我們甚至對 Jest 測試部分都使用了 Flow 來定義類型),不過我們還是會對此進行進一步的分解。在第 4 行,我們引入了地圖的 Reducer 函數(js/reducers/maps.js) 這樣便能在單元測試中直接調用(Reducer 函數作為pure functions 能夠很容易完成這些)。
第一段測試代碼位于第 8 行,目的是確保 Recucer 函數返回一個空數組。觀察 js/reducers/maps 處 Reducer 代碼,你會發現 state 并未做任何初始化,因此我們會期待期待單元測試結果為返回空數組。
第二段測試代碼位于第 12 行,目的是確保地圖數據被解析 API 檢索到的時候能夠被 Reducer 函數轉化成 state 樹中對應的正確結構。在這個測試中,我們使用假數據完全模擬真實 API 返回數據結構,這將避免API連接問題導致的測試失敗。
現在我們已經令跑 Jest 測試成為開發工作流程(比如每次提交 git 前)的一部分。這樣我們能確保對現有代碼的改動不會令 app 默默陣亡。
由于 Redux Reducers 會改變 state 樹,不引入 bug 變得絕對至關重要。由 state 改變引發的 bug 很容易被忽略。因為它們并 不會造成功能不可用,而僅僅只是造成往 Parse Server 發送錯誤數據的問題。 Reducer 函數的 pure function 特性令它們成為回歸測試的理想對象,因為每次我們都可以準確預知它們會如何執行。
調試
在你試圖對 bug 定位或者修復時,手邊有一些調試工具的話。我們已經介紹了如何搭建app可視元素調試系統,那這么處理數據呢?
我們通過 Nuclite 和 Redux Logger middleware 來調用 Chrome 開發者工具( Chrome Developer Tools ),控制臺上展示類似 Actions 或 Reducers 中 state 變動這樣的新增 Redux 上下文( context ): 你可以看到我們是如何通過configurestore來啟動這個的:
/* from js/store/configureStore.js */
var createLogger = require('redux-logger');
...
var isDebuggingInChrome = __DEV__ && !!window.navigator.userAgent;
var logger = createLogger({
predicate: (getState, action) => isDebuggingInChrome,
collapsed: true,
duration: true,
});
var createF8Store = applyMiddleware(thunk, promise, array, logger)(createStore);
function configureStore(onComplete: ?() => void) {
const store = autoRehydrate()(createF8Store)(reducers);
persistStore(store, {storage: AsyncStorage}, onComplete);
if (isDebuggingInChrome) {
window.store = store;
}
return store;
}
在第 5 行,我們通過一些選項創建了 Logger middleware 。 接著,在第 10 行我們調用 Redux 的 applyMiddleware() 函數開始應用 Logger middleware 。這樣,我們便可以在控制臺上看到日志輸出了。
在第4行我們使用全局變量 DEV 來觸發調試功能,通過布爾值的改動來在調試模式的開關間進行切換。這一舉措除了會決定 Logger middleware 創建后是否記錄日志(通過斷言選項),還會在第17行 拷貝當前 Store 到 Window object ,如此就可以更容易的通過控制臺來直接查看 Store 對象。
下一篇:構建 F8 2016 App 附錄 1:本地運行 App
來自:pockry