測試的種類有許多種,包括單元測試、整合測試。一般會先從單元測試做起,打好根基,也能加強效率高的小範圍的除錯。
在 React Native 中,有很多的理由應該盡量可能的使用 JavaScript 來寫程式碼,避免掉 Objective-C、Swift、Java 的程式碼,測試也是其中一個理由。要跑三個環境的單元測試想來大家都不願意,用 JavaScript 的話就可以統一使用 Jest 即可。
目前說到單元測試,會想到的不外乎以下三個選擇:
TJ Holowaychuk 開發的 Mocha 曾經是一代霸主,當時想到 JavaScript 單元測試就會直接想到它,兩年前沒什麼其他對手,至今也仍然為這三個之中下載數最高的。
不過現在看來它的原始碼比較老舊,從 v2.0
到 v3.0
花了近兩年時間,目前雖然穩定但看起來也比較沒有改進的動作。
在 Ava 跟 Jest 都已經能平行跑多個測試的情況下,相形失色。
雖然筆者大部分的新專案都使用 Jest,但如果有需要搭配 Karma Runner 跑在瀏覽器中的情況,無法使用 Jest (需要 Node 環境) 時還是會使用 Mocha。
Ava 則是 Node 界最有生產力的 sindresorhus 最看重的 Project 之一。
在大神加持伴隨著完整內建支援 ES6、平行跑測試、簡潔的 Assertion,乍看之下也是很不錯的工具。筆者只用了不到一個月的時間,雖然覺得略優於 Mocha 但卻也遇到一些問題。
Ava 最大的問題在於為完整社群開發的工具,不似 Jest 有 Facebook 支付全職員工在做事,也不似 Mocha 是 JS Foundation 的 Project 還有眾多的 Backer 與 Sponsor,自願者雖然辛苦但回應速度還是遠不及收錢辦事的開發者,這也是一個殘酷面。
Kent C. Dodds 也發了一篇 Migrating to Jest,寫了一些跟筆者經歷過的很類似的心路歷程,最後都換到 Jest。
在 2014 年,Jest 還是眾所抱怨的對象,Auto Mock 的功能沒有幫到太多的情況下,測試速度慢、卡住等等的問題很多,但 2016 年,Jest 就像是脫胎換骨了一樣,完全不像是同個東西:
(筆者用過 v0.6
非常的慘,現在的 v18
則很令人滿意)
除了可平行的跑測試,好用的 Watch Mode、jest.fn
產生的 Mock 跟 Assertion 處得相當融洽,更是有特有的 Snapshot Testing,用了就回不去了。
只要是程式都會有一些問題是難免,但 Jest 處理的速度是非常之快。在我曾經發過的這個 Issue 中,Christoph Pojer 處理的速度讓我非常印象深刻,修好到發布新版還不到三個工作天:
React Native v0.38
之後 react-native init
的時候就會內建 Jest 在專案裡面,該做的設定也都已經自動設定好。
在 <root>/__tests__
也能看到兩個預先擺放好的測試檔:
// <root>/__tests__/index.ios.js
import 'react-native';
import React from 'react';
import Index from '../index.ios.js';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const tree = renderer.create(
<Index />
);
});
直接執行:
npm test
就能執行測試囉。
沒有平台依賴性的 JavaScript 程式一般都很好測試,也不會跟在寫 Node 時的單元測試有太大的差別。不過 Component 就不一樣,有平台的依賴性,而寫單元測試時我們應該測試自己寫的程式,不應該去測試到 React Native 又或是其他的套件所寫的程式,所以只要測試 Render 出正確的結構,而不用真的去跑在手機模擬器上。
要測試 Render 出來的東西是否符合預期主要有以下兩種做法:
react-native
並使用 Shallow Renderingreact-test-renderer
做 Snapshot Testing而 Snapshot Testing 會稍微方便一些,當 UI 改來改去時,只要下 jest -u
就能更新 Snapshot 而不需要一再的修改很脆弱的測試
Snapshot Testing 相當簡單,只要用 renderer.create
去 Render 然後呼叫 toJSON
後丟去 toMatchSnapshot
Assertion。
import renderer from 'react-test-renderer';
const tree = renderer.create(
<Intro />
).toJSON();
expect(tree).toMatchSnapshot();
假設有一個 Intro
Component:
// __tests__/Intro-test.js
import 'react-native';
import React from 'react';
import Intro from '../Intro';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const tree = renderer.create(
<Intro />
).toJSON();
expect(tree).toMatchSnapshot();
});
會產生一個 .js.snap
的檔案,裡面會有剛剛 Value 的 Snapshot:
// __tests__/__snapshots__/Intro-test.js.snap
exports[`Intro renders correctly 1`] = `
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#F5FCFF",
"flex": 1,
"justifyContent": "center",
}
}>
<Text
style={
Object {
"fontSize": 20,
"margin": 10,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>
<Text
style={
Object {
"color": "#333333",
"marginBottom": 5,
"textAlign": "center",
}
}>
This is a React Native snapshot test.
</Text>
</View>
`;
這樣如果有改變的話,都能從 Git 的 Commit 上看到,也能在 Pull Request 進行 Review。
筆者一直認為,寫單元測試是寫出高品質的程式碼與琢磨架構設計的關鍵。唯有測試存在,才能安心的盡己所能去重構,讓程式碼擁有更好的未來性。