Widget 測試介紹

單元測試介紹 部分,我們學習了使用 test 這個 package 測試 Dart 類別的方法。為了測試 widget 類,我們需要使用 flutter_test package 提供的額外工具,這些工具是跟 Flutter SDK 一起發布的。

flutter_test package 提供了以下工具用於 widget 的測試:

  • WidgetTester,使用該工具可在測試環境下建立 widget 並與其互動。

  • testWidgets() 函式,此函式會自動為每個測試建立一個 WidgetTester,用來代替普通的 test 函式。

  • Finder 類,可以方便我們在測試環境下查詢 widgets。

  • Widget-specific Matcher 常數,該常數在測試環境下幫助我們驗證 Finder 是否定位到一個或多個 widgets。

如果覺得太複雜,別擔心!讓我們透過下面這些步驟把這些內容整合起來。

步驟:

  1. 新增一個 flutter_test 依賴

  2. 建立一個測試用的 widget

  3. 建立一個 testWidgets 測試方法

  4. 使用 WidgetTester 建立 widget

  5. 使用 Finder 查詢 widget

  6. 使用 Matcher 驗證 widget 是否正常工作

一. 新增一個 flutter_test 依賴

我們開始編寫測試之前,需要先給 pubspec.yaml 檔案的 dev_dependencies 段新增 flutter_test 依賴。如果使用指令行或編譯器新建一個 Flutter 專案,那麼依賴已經預設新增了。

dev_dependencies:
  flutter_test:
    sdk: flutter

二. 建立一個測試用的 Widget

接下來,我們需要建立一個可以測試的 widget!在此例中,我們建立了一個 widget 顯示一個 標題 (title)訊息 (message)

class MyWidget extends StatelessWidget {
  const MyWidget({
    super.key,
    required this.title,
    required this.message,
  });

  final String title;
  final String message;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}

三. 建立一個 testWidgets 測試方法

現在我們有了一個可以測試的 widget,可以開始編寫第一個測試了!第一步,我們用 flutter_test 這個 package 提供的 testWidgets() 函式定義一個測試。 testWidgets 函式可以定義一個 widget 測試並建立一個可以使用的 WidgetTester

我們的測試會驗證 MyWidget 是否顯示給定的標題和訊息。

void main() {
  // Define a test. The TestWidgets function also provides a WidgetTester
  // to work with. The WidgetTester allows you to build and interact
  // with widgets in the test environment.
  testWidgets('MyWidget has a title and message', (tester) async {
    // Test code goes here.
  });
}

四. 使用 WidgetTester 建立 Widget

下一步,為了在測試環境中建立 MyWidget,我們可以使用 WidgetTester 提供的 pumpWidget()方法,pumpWidget 方法會建立並渲染我們提供的 widget。

在這個範例中,我們將建立一個顯示標題『T』和訊息『M』的 MyWidget 範例。

void main() {
  testWidgets('MyWidget has a title and message', (tester) async {
    // Create the widget by telling the tester to build it.
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
  });
}

pump() 方法相關提示

初次呼叫 pumpWidget() 之後,WidgetTester 會提供其他方式來重建相同的 widget。這對使用 StatefulWidget 或者動畫會非常有用。

例如,如果我們點選呼叫 setState() 的按鈕,在測試環境中,Flutter 並不會自動重建你的 widget。我們需要用以下列舉的方法來讓 Flutter 再一次建立我們的 widget。

tester.pump(Duration duration)
排程一幀以觸發 widget 的重建。若指定了 Duration,那麼則會讓 clock 經過該時長之後排程一幀。它並不會因為持續時間大於一幀的時間而排程多幀。

tester.pumpAndSettle()
在給定期間內不斷重複呼叫 pump 直到完成所有繪製幀。一般需要等到所有動畫全部完成。

這些方法在建立週期中保證細粒度控制,這在測試中非常有用。

五. 使用 Finder 查詢 widget

現在讓我們在測試環境中建立 widget。我們需要用 Finder 透過 widget 樹來查詢 標題訊息 Text widgets,這樣可以驗證這些 Widgets 是否正確顯示。

為了實現這個目的,我們使用 flutter_test 這個 package 提供的頂級 find() 方法來建立我們的 Finders。因為我們要查詢的是 Text widgets,所以可以使用 find.text() 方法。

關於 Finder classes 的更多訊息,請參閱 定位到目標 Widgets 章節。

void main() {
  testWidgets('MyWidget has a title and message', (tester) async {
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));

    // Create the Finders.
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');
  });
}

六. 使用 Matcher 驗證 widget 是否正常工作

最後,讓我們來用 flutter_test 提供的 Matcher 常數驗證 Text widgets 顯示的標題和訊息。Matcher 類是 test 套件裡的核心部分,它提供一種通用方法來驗證給定值是否符合我們的預期。

在這個範例中,我們要確保 widget 只在螢幕中出現一次。因此,可以使用 findsOneWidget Matcher

void main() {
  testWidgets('MyWidget has a title and message', (tester) async {
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // Use the `findsOneWidget` matcher provided by flutter_test to verify
    // that the Text widgets appear exactly once in the widget tree.
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

其他的 Matchers

除了 findsOneWidgetflutter_test 還為常見情況提供了其他的 matchers。

findsNothing
驗證沒有可被查詢的 widgets。

findsWidgets
驗證一個或多個 widgets 被找到。

findsNWidgets
驗證特定數量的 widgets 被找到。

matchesGoldenFile
驗證渲染的 widget 是否與特定的影象對應(「目標檔案」測試)。

完整範例

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

void main() {
  // Define a test. The TestWidgets function also provides a WidgetTester
  // to work with. The WidgetTester allows building and interacting
  // with widgets in the test environment.
  testWidgets('MyWidget has a title and message', (tester) async {
    // Create the widget by telling the tester to build it.
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));

    // Create the Finders.
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // Use the `findsOneWidget` matcher provided by flutter_test to
    // verify that the Text widgets appear exactly once in the widget tree.
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

class MyWidget extends StatelessWidget {
  const MyWidget({
    super.key,
    required this.title,
    required this.message,
  });

  final String title;
  final String message;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}