iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
Mobile Development

從零開始以Flutter打造跨平台聊天APP系列 第 19

Day-19 在 Flutter 中使用 Widget Test 測試畫面

  • 分享至 

  • xImage
  •  

Generated from Stable Diffusion 3 Medium

在 Flutter 中,「測試」是確保功能正常、效能穩定的重要步驟之一。Flutter 提供了不同種的測試方法,包括「單元測試 (unit test)」、widget 測試以及整合測試 (Integration testing)。其中,單元測試的概念,我們在 Day-15 在 Flutter 中自動生成 JSON 序列化程式碼並撰寫單元測試 有稍微提到,可以在 test/ 資料夾設自己設計測試函式,並使用 flutter test 進行測試。單元測試主要在測試一個函式在特定輸入下是否能輸出預期值來保證該函式在修改後仍能正常運作。而 widget test 則是用來測試 UI 在一系列互動下是否仍能符合預期。

widget_test 是用來測試單個 widget 的行為,模擬 widget 的生命週期、繪製畫面,以及用戶與 UI 的互動,適合用來測試視覺元素及檢查 widget 的狀態變化。

本次的範例程式碼:https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/widget_test_practice

範例

首先,我們觀察一個最簡單的例子,使用 flutter create 來建立一個全新的專案

flutter create widget_test_practice
.
└── widget_test_practice/
    ├── lib/
    │   └── main.dart
    └── test/
        └── widget_test.dart

建立完新專案後,就是大家熟悉的 floatButton 點一下中間數字會加一的預設 APP。相信大家對 main.dart 裡每行指令都瞭若指掌了。接下來我們把目光放在 test/widget_test.dart 中:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:widget_test_practice/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(const MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

與之前所講的單元測試不同,widget test 使用 testWidgets 這個函式來做測試,而不是 testtestWidgets 中的第一個參數用來指定測試的名稱,第二個參數則是主要測試的邏輯,是一個異步函式。

在測試的一開始,我們可以用 tester.pumpWidget 來渲染畫面,他的做用就像 lib/main.dartmain() 函式中的 runApp() 函式。首先我們先渲染 MyApp(),接著我們使用 find.text() 來尋找 MyApp() 畫面中的 Text 物件或 EditableText 物件。我們也可以調整參數使用尋找 RichText 物件

Finder text(
  String text, {
  bool findRichText = false
  bool skipOffstage = true,
})

由於一開始渲染出來的畫面為 0,所以應該會找到一個值

expect(find.text('0'), findsOneWidget);

而因為畫面中沒有顯示為 1Text, EditableText 物件,因此預期是找不到

expect(find.text('1'), findsNothing);

接著我們模擬使用者點擊 floatingActionButton,由於主畫面中的 floatingActionButton 含有一個 Icon 物件,且整個 MyApp 只有這個按扭有這個 Icons.add 因此我們可以用 find.byIcon 來定位該按鈕。並且呼叫 tester.tap 來模擬使用者點擊。

FloatingActionButton(
  onPressed: _incrementCounter,
  tooltip: 'Increment',
  child: const Icon(Icons.add),
)

tap 函式會模擬點擊在 Widget 的正中間。當 warnIfMissed 設為 true 時,如果 Widget 不在螢幕上、或者為透明、或者被設為禁用狀態而無法點擊時,就會噴出警告訊息

Future<void> tap(
  FinderBase<Element> finder, {
  int? pointer,
  int buttons = kPrimaryButton,
  bool warnIfMissed = true,
  PointerDeviceKind kind = PointerDeviceKind.touch,
}

當我們模擬點擊後,還需要使用 tester.pump() 等待畫面重繪。由於我們已經將等待畫面更新完畢,此時畫面中間的數字會從 0 變成 1。我們可以再呼叫 expect 檢查 UI 是否符合預期。

使用 find.byKey 以 Key 值尋找 Widget

find 的行為有點類似 Javascript 中尋找 DOM 節點的方法 document.getElementBy... 。那麼在 Javascript 中我們有一種方法是直接對 ID 做搜尋,那麼在 Flutter 中是否也有呢?

我們可以直接針對按鈕加上 key

FloatingActionButton(
  key: const ValueKey("increment-button"),
  onPressed: _incrementCounter,
  tooltip: 'Increment',
  child: const Icon(Icons.add),
)

而模擬觸發時則可以利用 find.byKey 來尋找

await tester.tap(find.byIcon(Icons.add));

如果我們有自訂的 Widget,也可以直接使用 find.byType 來定位

class MyButton extends StatelessWidget {
  // ...
}
await tester.tap(find.byType(MyButton));

靈活的 finder

如同 Javascript 對 DOM 的定位,finder 也可以在 widget 中選擇上一層的 widget 或下一層的 widget,比如我們可以使用 find.descendant() 針對某個 widget 下層的所有 widget 中進行尋找;而 find.ancestor() 則可以在某個 widget 的上層 widget 中進行尋找。

舉例:假設我們將 FloatingActionButton 內的 Icon 也增加一個 key

floatingActionButton: FloatingActionButton(
    key: const ValueKey("increment-button"),
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: const Icon(
      Icons.add,
      key: ValueKey("increment-icon"),
    ),
  ),

我們可以從 increment-icon 的上層節點中尋找一個 TypeFloatingActionButton 的 Widget

find.ancestor(
    of: find.byKey(const ValueKey("increment-icon")),
    matching: find.byType(FloatingActionButton)));

上一篇
Day-18 在 Flutter 中使用 pointycastle 進行端對端加密
下一篇
Day-20 實作(1) Flutter 建立註冊與登入畫面
系列文
從零開始以Flutter打造跨平台聊天APP25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言