如何測試 Flutter 應用程式

1. 簡介

Flutter 是 Google 的 UI 工具包,可讓您透過單一程式碼集,建構美觀且以原生方式編譯的應用程式,適用於行動、網頁和電腦。

在本程式碼研究室中,您將建構及測試簡單的 Flutter 應用程式。應用程式會使用 Provider 套件來管理狀態。

課程內容

  • 如何使用小工具測試架構建立小工具測試
  • 如何建立整合測試,以便使用 integration_test 程式庫測試應用程式的 UI 和效能
  • 如何在單元測試的協助下測試資料類別 (供應商)

建構項目

在本程式碼研究室中,您將開始建構內含項目清單的簡易應用程式。我們提供原始碼,方便您直接進行測試。這個應用程式支援下列作業:

  • 正在將項目加入收藏
  • 查看我的最愛清單
  • 正在從收藏項目清單中移除項目

應用程式完成後,您將編寫下列測試:

  • 單元測試,用於驗證新增和移除作業
  • 小工具測試首頁和最愛網頁
  • 使用整合測試,測試整個應用程式的 UI 和效能測試

在 Android 上執行應用程式的 GIF

您希望從本程式碼研究室學到什麼?

我對這個主題不太熟悉,且希望概略瞭解相關資訊。 我對這個主題瞭若指掌,但希望複習一下。 我想尋找可以在專案中使用的程式碼範例。 我想查看特定事項的說明。

2. 設定 Flutter 開發環境

您需要使用兩項軟體:Flutter SDK編輯器

您可以使用下列任一裝置執行程式碼研究室:

  • 將實體 AndroidiOS 裝置接上電腦,並設為開發人員模式。
  • iOS 模擬器 (需要安裝 Xcode 工具)。
  • Android Emulator (需要在 Android Studio 中設定)。
  • 瀏覽器 (必須使用 Chrome 進行偵錯)。
  • 下載 WindowsLinuxmacOS 桌面應用程式。您必須在要部署的平台上進行開發。因此,如果您想要開發 Windows 電腦版應用程式,就必須在 Windows 上進行開發,以便存取適當的建構鏈結。如要進一步瞭解作業系統的特定需求,請參閱 docs.flutter.dev/desktop

3. 開始使用

建立新的 Flutter 應用程式並更新依附元件

本程式碼研究室著重於測試 Flutter 行動應用程式。您可以使用複製及貼上的來源檔案快速建立要測試的應用程式。本程式碼研究室的其他部分則著重於學習不同類型的測試。

a3c16fc17be25f6c.png根據「開始使用您的第一個 Flutter 應用程式」一文的操作說明,建立簡單的範本 Flutter 應用程式,或在指令列中操作 (如下所示)。

$ flutter create --empty testing_app
Creating project testing_app...
Resolving dependencies in `testing_app`... 
Downloading packages... 
Got dependencies in `testing_app`.
Wrote 128 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your empty application, type:

  $ cd testing_app
  $ flutter run

Your empty application code is in testing_app/lib/main.dart.

a3c16fc17be25f6c.png在指令列中加入 Pub 依附元件。

  • provider,可輕鬆管理狀態。
  • integration_test:用於在裝置和模擬器上自動進行 Flutter 程式碼測試
  • 使用進階 API flutter_driver,用於測試在實際裝置和模擬器上執行的 Flutter 應用程式。
  • test,適用於一般測試工具。
  • go_router 用於處理應用程式導覽。
$ cd testing_app
$ flutter pub add provider go_router dev:test 'dev:flutter_driver:{"sdk":"flutter"}' 'dev:integration_test:{"sdk":"flutter"}'
Resolving dependencies... 
Downloading packages... 
+ _fe_analyzer_shared 67.0.0 (68.0.0 available)
+ analyzer 6.4.1 (6.5.0 available)
+ args 2.5.0
+ convert 3.1.1
+ coverage 1.7.2
+ crypto 3.0.3
+ file 7.0.0
+ flutter_driver 0.0.0 from sdk flutter
+ flutter_web_plugins 0.0.0 from sdk flutter
+ frontend_server_client 4.0.0
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ glob 2.1.2
+ go_router 14.0.2
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ integration_test 0.0.0 from sdk flutter
+ io 1.0.4
+ js 0.7.1
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
+ logging 1.2.0
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ mime 1.0.5
+ nested 1.0.0
+ node_preamble 2.0.2
+ package_config 2.1.0
+ platform 3.1.4
+ pool 1.5.1
+ process 5.0.2
+ provider 6.1.2
+ pub_semver 2.1.4
+ shelf 1.4.1
+ shelf_packages_handler 3.0.2
+ shelf_static 1.1.2
+ shelf_web_socket 1.0.4
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.12
+ sync_http 0.3.1
+ test 1.25.2 (1.25.4 available)
  test_api 0.7.0 (0.7.1 available)
+ test_core 0.6.0 (0.6.2 available)
+ typed_data 1.3.2
+ watcher 1.1.0
+ web 0.5.1
+ web_socket_channel 2.4.5
+ webdriver 3.0.3
+ webkit_inspection_protocol 1.2.1
+ yaml 3.1.2
Changed 44 dependencies!
9 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

下列依附元件應已新增至 pubspec.yaml

pubspec.yaml

name: testing_app
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.4.0-0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.2
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  test: ^1.25.2
  flutter_driver:
    sdk: flutter
  integration_test:
    sdk: flutter

flutter:
  uses-material-design: true

a3c16fc17be25f6c.png在所選程式碼編輯器中開啟專案,然後執行應用程式。或者,您也可以在指令列中執行該工具,如下所示。

$ flutter run

4. 建構應用程式

接下來,您可以建構應用程式以便進行測試。這個應用程式包含下列檔案:

  • lib/models/favorites.dart:建立收藏清單的模型類別
  • lib/screens/favorites.dart:建立收藏清單的版面配置
  • lib/screens/home.dart:建立項目清單
  • lib/main.dart:應用程式啟動時的主要檔案

首先,請在 lib/models/favorites.dart 中建立 Favorites 模型

a3c16fc17be25f6c.pnglib 目錄中建立名為 models 的新目錄,然後建立名為 favorites.dart 的新檔案。在該檔案中,加入下列程式碼:

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

lib/screens/favorites.dart 中新增「我的收藏」頁面

a3c16fc17be25f6c.pnglib 目錄中建立名為 screens 的新目錄,然後在該目錄中建立名為 favorites.dart 的新檔案。在該檔案中,加入下列程式碼:

lib/screens/favorites.dart

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

import '../models/favorites.dart';

class FavoritesPage extends StatelessWidget {
  const FavoritesPage({super.key});

  static String routeName = 'favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  const FavoriteItemTile(this.itemNo, {super.key});

  final int itemNo;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: const Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

新增lib/screens/home.dart的首頁

a3c16fc17be25f6c.pnglib/screens 目錄中,建立另一個名為 home.dart 的新檔案。在 lib/screens/home.dart 中加入下列程式碼:

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/favorites.dart';
import 'favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () {
              context.go('/${FavoritesPage.routeName}');
            },
            icon: const Icon(Icons.favorite_border),
            label: const Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(this.itemNo, {super.key});

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? const Icon(Icons.favorite)
              : const Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: const Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

取代 lib/main.dart 的內容

a3c16fc17be25f6c.png使用以下程式碼取代 lib/main.dart 的內容:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'models/favorites.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';

void main() {
  runApp(const TestingApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: HomePage.routeName,
      builder: (context, state) {
        return const HomePage();
      },
      routes: [
        GoRoute(
          path: FavoritesPage.routeName,
          builder: (context, state) {
            return const FavoritesPage();
          },
        ),
      ],
    ),
  ],
);

class TestingApp extends StatelessWidget {
  const TestingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp.router(
        title: 'Testing Sample',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: Colors.deepPurple,
          ),
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

應用程式現已完成,但尚未進行測試。

a3c16fc17be25f6c.png執行應用程式。畫面應如以下螢幕截圖所示:

b74f843e42a28b0f.png

應用程式會顯示項目清單。輕觸任何一列的心形圖示,即可填入愛心,並將物品新增至收藏清單。AppBar 上的「Favorites」按鈕會將您導向第二個畫面,當中包含最愛清單。

現在可以測試應用程式了。您將在下一個步驟中開始測試應用程式。

5. 單元測試供應商

首先,單元測試 favorites 模型。什麼是單元測試?單元測試會確認每個軟體「單元」 (如功能、物件或小工具) 都能正確執行預期工作。

Flutter 應用程式中的所有測試檔案 (整合測試除外) 會放在 test 目錄中。

移除「test/widget_test.dart

a3c16fc17be25f6c.png開始測試前,請先刪除 widget_test.dart 檔案。您將新增自己的測試檔案。

建立新的測試檔案

首先,請在 Favorites 模型中測試 add() 方法,確認是否已將新項目加入清單,且清單會顯示這項變更。按照慣例,test 目錄中的目錄結構會模仿 lib 目錄和 Dart 檔案中的相同名稱,後面加上 _test

a3c16fc17be25f6c.pngtest 目錄中建立 models 目錄。在這個新的目錄中,建立包含以下內容的 favorites_test.dart 檔案:

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });    
  });
}

Flutter 測試架構可讓您繫結至群組中彼此相關的類似測試。單一測試檔案可以有多個群組,用於測試 lib 目錄中對應檔案的不同部分。

test() 方法使用兩個位置參數:測試的 description 和您實際編寫測試的 callback

a3c16fc17be25f6c.png測試從清單中移除項目。在同一個 Testing App Provider 群組中插入下列測試:

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

執行測試

a3c16fc17be25f6c.png在指令列中,前往專案的根目錄,並輸入下列指令:

$ flutter test test/models/favorites_test.dart 

如果一切正常,畫面會顯示類似下圖的訊息:

00:06 +2: All tests passed!                                                    

完整的測試檔案:test/models/favorites_test.dart

如要進一步瞭解單元測試,請參閱「單元測試簡介」。

6. 小工具測試

在這個步驟中,您需要新增程式碼來測試小工具。Flutter 專屬的小工具測試功能,可以在獨立模式下測試每個小工具。這個步驟會分別測試 HomePageFavoritesPage 畫面。

小工具測試會使用 testWidget() 函式,而非 test() 函式。與 test() 函式類似,testWidget() 函式會使用 description,callback 這兩個參數,但回呼會採用 WidgetTester 做為引數。

小工具測試使用 TestFlutterWidgetsBinding,這個類別可為執行中應用程式的小工具提供相同資源,例如瞭解螢幕大小、可以排定動畫時間,但無法在應用程式中執行。相反地,虛擬環境是用來將小工具例項化,然後執行測試結果。這裡 pumpWidget 會指示架構掛接及測量特定小工具,以啟動程序,就像在應用程式中執行一樣。

小工具測試架構提供尋找工具,用於尋找 text()byType()byIcon(). 等小工具。架構也提供比對器,可用來驗證結果。

首先,請測試 HomePage 小工具。

建立新的測試檔案

第一個測試可驗證捲動 HomePage 是否正常運作。

a3c16fc17be25f6c.pngtest 目錄中建立新檔案,並命名為 home_test.dart。在新建立的檔案中,加入下列程式碼:

test/home_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: const MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

createHomeScreen() 函式可用來建立應用程式,載入要在 MaterialApp 中測試的小工具,並納入 ChangeNotifierProvider。HomePage 小工具需要兩個小工具都顯示在小工具樹狀結構的上方,才能沿用並存取其提供的資料。此函式會以參數的形式傳遞至 pumpWidget() 函式。

接下來,測試架構是否能找到畫面上顯示的 ListView

a3c16fc17be25f6c.pnghome_test.dart 中加入下列程式碼片段:

test/home_test.dart

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {  
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });                                                
  // END OF NEW CONTENT

    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
});

執行測試

首先,請以執行單元測試的方式執行測試,請使用下列指令:

$ flutter test test/home_test.dart 

測試應該能快速執行,您應該會看到類似下方的訊息:

00:02 +2: All tests passed!                                                    

您也可以使用裝置或模擬器執行小工具測試,查看正在執行的測試。並讓您使用熱重新啟動。

a3c16fc17be25f6c.png連接裝置或啟動模擬器。您也可以以電腦應用程式的形式執行測試。

a3c16fc17be25f6c.png在指令列中,前往專案的根目錄,並輸入下列指令:

$ flutter run test/home_test.dart 

您可能需要選取要執行測試的裝置。遇到這種情況時,請按照指示選取裝置:

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 

如果一切運作正常,您應該會看到類似以下的輸出內容:

Launching test/home_test.dart on Linux in debug mode...
Building Linux application...                                           
flutter: 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Linux...                                    62ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:00 +1: Home Page Widget Tests Testing Scrolling
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:02 +2: All tests passed!

接下來,您需要變更測試檔案,然後按下 Shift + R,以熱重新啟動應用程式並重新執行所有測試。請勿停止應用程式。

a3c16fc17be25f6c.png在用來測試 HomePage 小工具的群組中加入更多測試。將下列測試複製到您的檔案中:

test/home_test.dart

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

這項測試會驗證輕觸IconButton (開放心形) 如何從 Icons.favorite_border (開放的心) 變成 Icons.favorite (填滿的愛心),然後在再次輕觸時返回Icons.favorite_border

a3c16fc17be25f6c.png輸入「Shift + R」。這個指令會重新啟動應用程式,並重新執行所有測試。

完整的測試檔案:test/home_test.dart.

a3c16fc17be25f6c.png請使用相同程序使用下列程式碼測試 FavoritesPage。按照相同的步驟執行,然後執行。

test/favorites_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

這項測試可驗證當按下關閉 (移除) 按鈕時,項目是否消失。

如要進一步瞭解小工具測試,請前往:

7. 使用整合測試測試應用程式 UI

整合測試可用來測試應用程式的個別部分如何共同運作。integration_test 程式庫可用來在 Flutter 中執行整合測試。這是 Flutter 的 Selenium WebDriver、Protractor、Espresso 或 Earl Gray。套件會在內部使用 flutter_driver 執行裝置上的測試。

在 Flutter 中編寫整合測試與編寫小工具測試類似,差別在於測試會在行動裝置、瀏覽器或電腦應用程式 (稱為目標裝置) 上執行整合測試。

編寫測試

a3c16fc17be25f6c.png在專案的根目錄中建立名為 integration_test 的目錄,然後在該目錄中建立名為 app_test.dart 的新檔案。

integration_test/app_test.dart

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

void main() {
  group('Testing App', () {
    testWidgets('Favorites operations test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final iconKeys = [
        'icon_0',
        'icon_1',
        'icon_2',
      ];

      for (var icon in iconKeys) {
        await tester.tap(find.byKey(ValueKey(icon)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Added to favorites.'), findsOneWidget);
      }

      await tester.tap(find.text('Favorites'));
      await tester.pumpAndSettle();

      final removeIconKeys = [
        'remove_icon_0',
        'remove_icon_1',
        'remove_icon_2',
      ];

      for (final iconKey in removeIconKeys) {
        await tester.tap(find.byKey(ValueKey(iconKey)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Removed from favorites.'), findsOneWidget);
      }
    });
  });
}

執行測試

a3c16fc17be25f6c.png連接裝置或啟動模擬器。您也可以以電腦應用程式的形式執行測試。

a3c16fc17be25f6c.png在指令列中,前往專案的根目錄,並輸入下列指令:

$ flutter test integration_test/app_test.dart

如果一切正常,畫面會顯示類似以下的輸出內容:

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
00:00 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                B00:08 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                
00:26 +1: All tests passed!

8. 使用 Flutter 驅動程式測試應用程式效能

編寫效能測試

在 integration_test 資料夾中建立名為 perf_test.dart 的新測試檔案,其中含有以下內容:

integration_test/perf_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App Performance', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

    testWidgets('Scrolling test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final listFinder = find.byType(ListView);

      await binding.traceAction(() async {
        await tester.fling(listFinder, const Offset(0, -500), 10000);
        await tester.pumpAndSettle();

        await tester.fling(listFinder, const Offset(0, 500), 10000);
        await tester.pumpAndSettle();
      }, reportKey: 'scrolling_summary');
    });
  });
}

ensureInitialized() 函式會驗證整合測試驅動程式是否已初始化,並視需要重新初始化。將 framePolicy 設為 fullyLive 有助於測試動畫程式碼。

這項測試會快速捲動清單項目,然後向上捲動。traceAction() 函式會記錄動作並產生時間軸摘要。

擷取成效結果

如要擷取結果,建立名為 test_driver 的資料夾,其中含有名為 perf_driver.dart 的檔案,然後加入以下程式碼:

test_driver/perf_driver.dart

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
            data['scrolling_summary'] as Map<String, dynamic>);

        final summary = driver.TimelineSummary.summarize(timeline);

        await summary.writeTimelineToFile(
          'scrolling_summary',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

執行測試

a3c16fc17be25f6c.png連接裝置或啟動模擬器。

a3c16fc17be25f6c.png在指令列中,前往專案的根目錄,並輸入下列指令:

$ flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/perf_test.dart \
  --profile \
  --no-dds

如果一切正常,畫面會顯示類似以下的輸出內容:

Running "flutter pub get" in testing_app...
Resolving dependencies... 
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!
Running Gradle task 'assembleProfile'...                         1,379ms
✓  Built build/app/outputs/flutter-apk/app-profile.apk (14.9MB).
Installing build/app/outputs/flutter-apk/app-profile.apk...        222ms
I/flutter ( 6125): 00:04 +1: Testing App Performance (tearDownAll)
I/flutter ( 6125): 00:04 +2: All tests passed!
All tests passed.

測試成功完成後,專案根目錄的建構目錄會包含兩個檔案:

  1. scrolling_summary.timeline_summary.json 包含摘要。使用任何文字編輯器開啟檔案,查看其中的資訊。
  2. scrolling_summary.timeline.json 包含完整的時間軸資料。

如要進一步瞭解整合測試,請前往:

9. 恭喜!

您已完成程式碼研究室,並已瞭解測試 Flutter 應用程式的不同方式。

目前所學內容

  • 如何在單元測試的協助下測試供應商
  • 如何使用小工具測試架構測試小工具
  • 如何使用整合測試來測試應用程式的 UI
  • 如何使用整合測試測試應用程式效能

如要進一步瞭解如何在 Flutter 中進行測試,請前往