今天我們要從單元測試進入 Widget Test 的部分了,我們花了十幾天的時間介紹 Dart 的單元測試,也介紹了許多測試相關的概念與技巧,單元測試是最容易寫的測試,其中的概念多多少少也可以運用在 Widget Test 或其他類型測試中,讓我們直接開始介紹 Widget Test 吧。
在 Flutter 官方文件中介紹 Widget Test 是一種 Component Test,透過模擬使用者操作 UI 的行為,然後驗證畫面結果是否符合預期,有點像是 End to End 測試。在 Widget Test 中,雖然測試會模擬使用者的操作畫面,但實際上在執行 Widget Test 並不會真的看到畫面,也不會真的去打遠端 Server 的 API,所以 Widget Test 的執行速度十分接近單元測試。
Unit | Widget | Integration | |
---|---|---|---|
Confidence | Low | Higher | Highest |
Maintenance cost | Low | Higher | Highest |
Dependencies | Few | More | Most |
Execution speed | Quick | Quick | Slow |
出處:https://docs.flutter.dev/testing/overview
Flutter 是一個 UI 框架,在開發的時候,假設架構使用 Clean Architecture,我們雖然可以把邏輯都封裝到 Adapter 層或 Use Case 層,甚至 Entity 層,但是 UI 層還是多少會存在著操作 Adapter 層 API 的整合邏輯。此時,使用 Widget Test 來測試,才能測試到這些整合邏輯,也會比單元測試要來的接近實際情況。
那Widget Test 是不是可以取代單元測試呢?答案顯然不是,Widget Test 看起來美好,實際繼上還是有許多不方便的地方,像是除錯比較不方便,或者隨著測試的越外層,測試需要準備的資料也越多,寫起測試來肯定不像單元測試那樣順暢,維護比較麻煩。但是其實這些問題,隨著我們的持續增加我們的測試經驗與技巧,再加上善用 IDE 工具,還是能減少撰寫 Widget Test 的時間,降低開發成本。
在我們用 flutter 建新專案時,裡頭預設就會包含一個簡單的例子與 Widget Test,讓我們來看看這個簡單的例子。
在這個簡單的例子中,每當使用者按一次按鈕,畫面中的數字就會加 1。在範例測試中,也是依循著這個邏輯。
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
與單元測試類似,我們一樣是把 Widget Test 的測試案例放在 main 方法中,但是與單元測試不一樣的是,Widget Test 使用 testWidgets 方法來測試,而不是 test。在 testWidgets 的第二個參數會傳入非同步方法,這個非同步方法中有一個 WidgetTester 的參數,這個 WidgetTester 就是我們主要拿來與 Widget 互動的工具。
與單元測試建立 SUT 類似,我們在 Widget Test 中,需要決定要測試哪個 Widget,然後用 WidgetTester.pumpWidget 將 Widget 畫出來。
await tester.pumpWidget(const MyApp());
接著就可以用 WidgetTester 的 tap 方法模擬使用者點擊 Icon,再 tap 方法中,我們需要使用 Finder 幫我們找出 Icon,把他傳入 tap 方法中,讓程式執行點擊後的動作,使 count 加 1。
await tester.tap(find.byIcon(Icons.add));
雖然我們成功地把 count + 1,但是在測試中的畫面還是維持 0,因為 Widget Test 並不會自己刷新畫面,需要我們呼叫 WidgetTester.pump 方法,主動通知 WidgetTester 刷新畫面。
await tester.pump();
最後我們就可以用 expect 來驗證畫面數字是不是變成 1 了
expect(find.text('1'), findsOneWidget);
上面這個例子是十分簡單的 Widget Test 範例,在未來的幾天文章中,我們會介紹如何 Finder 找出各種不同的 Widget 與如何模擬各種使用者操作,透過組合 Finder 與 WidgetTester ,我們就能模擬大部分的情境了。
Widget Test 與單元測試在測試的結構上,兩者並沒有多大差別。在單元測試中,我們思考的是要怎麼測試 SUT 的行為,呼叫哪些方法,驗證哪些狀態。來到 Widget Test,我們則是在思考要測試哪個畫面的行為,點擊哪些按鈕,驗證畫面上出現哪些元素,本質上一樣是 3A 原則。
寫單元測試時,只要熟悉單元測試的概念,即便不熟悉 Dart 語法或 API,我們寫起來也不會有太大問題,因為單元測試是測試邏輯,對於語言的依賴度不大。但是寫 Widget Test 時,我們測試的角度就是從畫面出發,思考使用者與畫面如何互動,最後在畫面上產生何種結果,這就與單元測試有很大不同。
在單元測試中,我們使用的測試 API 基本上只有 expect 與測試替身,但是在 Widget Test 中,我們除了單元測試會用到的 API 之外,我們還要了解如何使用 Finder 找到想要找的 Widget,也要會使用 WidgetTester 的各種 API 模擬使用者操作,學習成本上多了不少。
今天是介紹 Widget Test 的第一天,在開發 Flutter 程式的過程中,如果只有使用單元測試,一個完整的使用者行為,可能就會被拆分成好幾段段不同測試,適時使用 Widget Test 反而會比較好測試,維護測試簡單一點。