測試
隨著你的程式碼庫擴展,你意想不到的小錯誤和邊緣情況可能會演變成更大的失敗。錯誤會導致不良的使用者體驗,並最終造成業務損失。防止脆弱程式設計的一種方法是在將程式碼發佈到實際環境之前進行測試。
在本指南中,我們將介紹不同的自動化方法,以確保你的應用程式如預期般運作,範圍從靜態分析到端對端測試。
為何測試
我們是人類,而人類會犯錯。測試很重要,因為它可以幫助你發現這些錯誤,並驗證你的程式碼是否正常運作。也許更重要的是,測試確保你的程式碼在未來繼續運作,當你新增功能、重構現有功能或升級專案的主要依賴項時。
測試的價值比你可能意識到的還要多。修正程式碼中錯誤的最佳方法之一是編寫一個失敗的測試來揭露它。然後,當你修正錯誤並重新執行測試時,如果它通過,則表示錯誤已修正,並且永遠不會重新引入程式碼庫中。
測試也可以作為新加入團隊成員的文件。對於以前從未見過程式碼庫的人來說,閱讀測試可以幫助他們理解現有程式碼是如何運作的。
最後但並非最不重要的一點是,更多的自動化測試意味著花在人工QA上的時間更少,從而釋放寶貴的時間。
靜態分析
提高程式碼品質的第一步是開始使用靜態分析工具。靜態分析會在您編寫程式碼時檢查錯誤,但無需執行任何程式碼。
- Linters 分析程式碼以捕獲常見錯誤,例如未使用的程式碼,並幫助避免陷阱,標記樣式指南的禁忌,例如使用 Tab 而不是空格(或反之亦然,取決於您的配置)。
- 類型檢查 確保您傳遞給函數的結構符合函數設計為接受的內容,例如,防止將字串傳遞給期望數字的計數函數。
React Native 預設配置了兩個這樣的工具:用於程式碼檢查的 ESLint 和用於類型檢查的 TypeScript。
編寫可測試的程式碼
要開始進行測試,您首先需要編寫可測試的程式碼。考慮一下飛機製造過程 - 在任何型號首次起飛以展示其所有複雜系統協同工作良好之前,都會對各個零件進行測試,以保證它們是安全且功能正確的。例如,機翼通過在極端負載下彎曲來進行測試;引擎零件會測試其耐用性;擋風玻璃會針對模擬的鳥擊進行測試。
軟體也是如此。與其將整個程式寫在一個包含許多程式碼行的大型檔案中,不如將程式碼寫在多個小型模組中,這樣您可以比測試組裝的整體更徹底地測試它們。透過這種方式,編寫可測試的程式碼與編寫乾淨、模組化的程式碼息息相關。
為了使您的應用程式更具可測試性,首先將應用程式的視圖部分(您的 React 元件)與您的業務邏輯和應用程式狀態分開(無論您是否使用 Redux、MobX 或其他解決方案)。這樣,您可以保持您的業務邏輯測試(不應依賴您的 React 元件)獨立於元件本身,元件本身的工作主要是渲染您的應用程式 UI!
從理論上講,您可以更進一步將所有邏輯和資料獲取移出您的元件。這樣您的元件將專門用於渲染。您的狀態將完全獨立於您的元件。您的應用程式邏輯將在完全沒有任何 React 元件的情況下運作!
我們鼓勵您在其他學習資源中進一步探索可測試程式碼的主題。
編寫測試
在編寫可測試的程式碼之後,就該編寫一些實際的測試了!React Native 的預設範本隨附 Jest 測試框架。它包含一個針對此環境量身定制的預設,因此您可以立即開始工作,而無需調整配置和模擬—稍後會詳細介紹 模擬。您可以使用 Jest 來編寫本指南中介紹的所有類型的測試。
如果您進行測試驅動開發,您實際上是先編寫測試!這樣,您的程式碼的可測試性就得到了保證。
組織測試
您的測試應該簡短,理想情況下只測試一件事。讓我們從一個使用 Jest 編寫的單元測試範例開始
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});
測試由傳遞給 it
函數的字串描述。請仔細編寫描述,使其清楚地說明正在測試的內容。盡力涵蓋以下內容
- Given - 某些先決條件
- When - 由您正在測試的函數執行的某些操作
- Then - 預期的結果
這也稱為 AAA(Arrange, Act, Assert)。
Jest 提供了 describe
函數來幫助組織您的測試。使用 describe
將屬於同一功能的所有測試組合在一起。如果需要,可以巢狀描述。您常用的其他函數是 beforeEach
或 beforeAll
,您可以使用它們來設定您正在測試的物件。在 Jest API 參考 中閱讀更多內容。
如果您的測試有很多步驟或許多期望,您可能需要將其拆分為多個較小的測試。此外,請確保您的測試彼此完全獨立。您的套件中的每個測試都必須可以單獨執行,而無需先執行其他測試。相反,如果您一起執行所有測試,則第一個測試不得影響第二個測試的輸出。
最後,作為開發人員,我們喜歡我們的程式碼運作良好且不會崩潰。對於測試來說,情況通常恰恰相反。將失敗的測試視為一件好事! 當測試失敗時,通常表示有些地方不對勁。這讓您有機會在問題影響使用者之前修復它。
單元測試
單元測試涵蓋程式碼的最小部分,例如單個函數或類別。
當被測試的物件有任何依賴項時,您通常需要模擬它們,如下一段所述。
單元測試的優點是它們編寫和執行速度很快。因此,在您工作時,您可以快速獲得有關您的測試是否通過的回饋。Jest 甚至有一個選項可以持續執行與您正在編輯的程式碼相關的測試:監看模式。
模擬
有時,當您測試的物件具有外部依賴項時,您會想要「模擬它們」。「模擬」是指您用自己的實作替換程式碼的某些依賴項。
通常,在您的測試中使用真實物件比使用模擬更好,但在某些情況下這是不可能的。例如:當您的 JS 單元測試依賴於用 Java 或 Objective-C 編寫的原生模組時。
想像一下,您正在編寫一個應用程式,顯示您所在城市的目前天氣,並且您正在使用一些外部服務或其他依賴項為您提供天氣資訊。如果服務告訴您正在下雨,您想顯示一張帶有雨雲的圖片。您不想在測試中呼叫該服務,因為
- 它可能會使測試變慢且不穩定(因為涉及網路請求)
- 該服務可能在您每次執行測試時傳回不同的資料
- 當您真的需要執行測試時,第三方服務可能會離線!
因此,您可以提供服務的模擬實作,有效地替換數千行程式碼和一些連接網際網路的溫度計!
Jest 提供了從函數級別到模組級別模擬的 模擬支援。
整合測試
在編寫較大型的軟體系統時,它的各個部分需要彼此互動。在單元測試中,如果您的單元依賴於另一個單元,您有時最終會模擬依賴項,並用假的依賴項替換它。
在整合測試中,真實的單個單元被組合在一起(與您的應用程式中相同),並一起測試以確保它們的協作如預期般運作。這並不是說模擬在這裡不會發生:您仍然需要模擬(例如,模擬與天氣服務的通訊),但您需要的模擬比單元測試中少得多。
請注意,關於整合測試含義的術語並不總是前後一致。此外,單元測試和整合測試之間的界線可能並不總是清楚。對於本指南,如果您的測試符合以下條件,則屬於「整合測試」
- 如上所述,組合您應用程式的幾個模組
- 使用外部系統
- 向其他應用程式發出網路呼叫(例如天氣服務 API)
- 執行任何類型的文件或資料庫 I/O
元件測試
React 元件負責渲染您的應用程式,使用者將直接與其輸出互動。即使您的應用程式的業務邏輯具有很高的測試覆蓋率並且是正確的,但如果沒有元件測試,您仍然可能會向使用者交付損壞的 UI。元件測試可以歸入單元測試和整合測試,但由於它們是 React Native 的核心部分,我們將單獨介紹它們。
對於測試 React 元件,您可能需要測試兩件事
- 互動:確保元件在使用者互動時行為正確(例如,當使用者按下按鈕時)
- 渲染:確保 React 使用的元件渲染輸出是正確的(例如,按鈕在 UI 中的外觀和位置)
例如,如果您有一個具有 onPress
監聽器的按鈕,您想要測試該按鈕是否正確顯示,以及元件是否正確處理按鈕的點擊。
有幾個函式庫可以幫助您測試這些
- React 的 Test Renderer 與其核心一起開發,提供了一個 React 渲染器,可用於將 React 元件渲染為純 JavaScript 物件,而無需依賴 DOM 或原生行動環境。
- React Native Testing Library 建構於 React 的測試渲染器之上,並添加了下一段中描述的
fireEvent
和query
API。
元件測試僅是在 Node.js 環境中運行的 JavaScript 測試。它們不考慮支援 React Native 元件的任何 iOS、Android 或其他平台程式碼。因此,它們無法讓您 100% 確信一切都為使用者運作。如果 iOS 或 Android 程式碼中存在錯誤,它們將無法找到它。
測試使用者互動
除了渲染一些 UI 之外,您的元件還處理事件,例如 TextInput
的 onChangeText
或 Button
的 onPress
。它們也可能包含其他函數和事件回調。考慮以下範例
function GroceryShoppingList() {
const [groceryItem, setGroceryItem] = useState('');
const [items, setItems] = useState<string[]>([]);
const addNewItemToShoppingList = useCallback(() => {
setItems([groceryItem, ...items]);
setGroceryItem('');
}, [groceryItem, items]);
return (
<>
<TextInput
value={groceryItem}
placeholder="Enter grocery item"
onChangeText={text => setGroceryItem(text)}
/>
<Button
title="Add the item to list"
onPress={addNewItemToShoppingList}
/>
{items.map(item => (
<Text key={item}>{item}</Text>
))}
</>
);
}
在測試使用者互動時,請從使用者的角度測試元件—頁面上顯示什麼?互動時會發生什麼變化?
作為經驗法則,優先使用使用者可以看到或聽到的東西
- 使用渲染的文字或 協助工具輔助程式 進行斷言
相反地,您應該避免
- 對元件 props 或狀態進行斷言
- testID 查詢
避免測試實作細節,例如 props 或狀態—雖然此類測試有效,但它們並不是針對使用者將如何與元件互動,並且往往會因重構而中斷(例如,當您想要重新命名某些內容或使用 Hooks 重寫類別元件時)。
React 類別元件尤其容易測試其實作細節,例如內部狀態、props 或事件處理程式。為了避免測試實作細節,請優先使用帶有 Hooks 的函數元件,這使得依賴元件內部結構更困難。
元件測試函式庫(例如 React Native Testing Library)通過仔細選擇提供的 API 來促進編寫以使用者為中心的測試。以下範例使用 fireEvent
方法 changeText
和 press
來模擬使用者與元件互動,以及查詢函數 getAllByText
,該函數在渲染的輸出中找到匹配的 Text
節點。
test('given empty GroceryShoppingList, user can add an item to it', () => {
const {getByPlaceholderText, getByText, getAllByText} = render(
<GroceryShoppingList />,
);
fireEvent.changeText(
getByPlaceholderText('Enter grocery item'),
'banana',
);
fireEvent.press(getByText('Add the item to list'));
const bananaElements = getAllByText('banana');
expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});
此範例不是測試當您呼叫函數時某些狀態如何變化。它測試的是當使用者在 TextInput
中更改文字並按下 Button
時會發生什麼情況!
測試渲染輸出
快照測試 是 Jest 啟用的一種進階測試。它是一個非常強大且低階的工具,因此建議在使用時格外注意。
「元件快照」是由 Jest 內建的自訂 React 序列化程式建立的類似 JSX 的字串。此序列化程式讓 Jest 將 React 元件樹轉換為人類可讀的字串。換句話說:元件快照是元件在測試執行期間產生的渲染輸出的文字表示形式。它可能看起來像這樣
<Text
style={
Object {
"fontSize": 20,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>
使用快照測試,您通常首先實作您的元件,然後執行快照測試。然後,快照測試會建立快照並將其儲存到您儲存庫中的檔案中,作為參考快照。然後,該檔案會被提交並在程式碼審查期間檢查。未來對元件渲染輸出的任何更改都將更改其快照,這將導致測試失敗。然後您需要更新儲存的參考快照才能使測試通過。該更改再次需要提交和審查。
快照有幾個弱點
- 對於作為開發人員或審查者的您來說,可能很難判斷快照中的更改是預期的還是錯誤的證據。尤其是大型快照很快就會變得難以理解,並且它們的附加價值會降低。
- 當建立快照時,在該點它被認為是正確的 - 即使在渲染輸出實際上是錯誤的情況下也是如此。
- 當快照失敗時,很想使用
--updateSnapshot
jest 選項更新它,而沒有適當地注意調查更改是否是預期的。因此需要一定的開發人員紀律。
快照本身並不能確保您的元件渲染邏輯是正確的,它們僅僅擅長防止意外更改,並檢查 React 樹中正在測試的元件是否收到預期的 props(樣式等)。
我們建議您僅使用小型快照(請參閱 no-large-snapshots
規則)。如果您想測試兩個 React 元件狀態之間的更改,請使用 snapshot-diff
。如有疑問,請優先選擇上一段中描述的明確期望。
端對端測試
在端對端 (E2E) 測試中,您從使用者的角度驗證您的應用程式在裝置(或模擬器/模擬器)上是否如預期般運作。
這是通過在發布配置中構建您的應用程式並針對其運行測試來完成的。在 E2E 測試中,您不再考慮 React 元件、React Native API、Redux 儲存或任何業務邏輯。這不是 E2E 測試的目的,並且在 E2E 測試期間您甚至無法訪問這些內容。
相反,E2E 測試函式庫允許您在應用程式的螢幕中查找和控制元素:例如,您可以實際點擊按鈕或將文字插入 TextInputs
,就像真實使用者會做的那樣。然後,您可以斷言螢幕中是否存在某個元素、它是否可見、它包含什麼文字等等。
E2E 測試讓您對應用程式的某些部分運作具有最高的信心。權衡包括
- 與其他類型的測試相比,編寫它們更耗時
- 它們執行速度較慢
- 它們更容易出現不穩定性(「不穩定」測試是指在程式碼沒有任何更改的情況下隨機通過和失敗的測試)
嘗試使用 E2E 測試涵蓋應用程式的重要部分:身份驗證流程、核心功能、付款等。對於應用程式的非重要部分,請使用更快的 JS 測試。您添加的測試越多,您的信心就越高,但是,您也將花費更多時間維護和運行它們。考慮權衡並決定什麼對您最好。
有幾種可用的 E2E 測試工具:在 React Native 社群中,Detox 是一個流行的框架,因為它是為 React Native 應用程式量身定制的。在 iOS 和 Android 應用程式領域中,另一個流行的函式庫是 Appium 或 Maestro。
總結
我們希望您喜歡閱讀並從本指南中學到了一些東西。您可以使用多種方法來測試您的應用程式。一開始可能很難決定使用哪種方法。但是,我們相信一旦您開始將測試添加到您出色的 React Native 應用程式中,一切都會變得有意義。所以您還在等什麼?提高您的覆蓋率!
連結
本指南最初由 Vojtech Novak 完全撰寫和貢獻。