Flutter アプリのテスト方法

1. はじめに

Flutter は、1 つのコードベースからネイティブにコンパイルして、モバイル、ウェブ、デスクトップの美しいアプリケーションを作成できる Google の UI ツールキットです。

この Codelab では、シンプルな Flutter アプリを作成してテストします。アプリでは、Provider パッケージを使用して状態を管理します。

学習内容

  • ウィジェット テスト フレームワークを使用してウィジェット テストを作成する方法
  • integration_test ライブラリを使用してアプリの UI とパフォーマンスをテストする統合テストの作成方法
  • 単体テストを使用してデータクラス(プロバイダ)をテストする方法

作成するアプリの概要

この Codelab では、最初に項目のリストがあるシンプルなアプリケーションを作成します。ソースコードは用意されているので、すぐにテストを開始できます。このアプリでは次の操作がサポートされます。

  • お気に入りへの項目の追加
  • お気に入りリストの表示
  • お気に入りリストからの項目の削除

アプリが完成したら、次のテストを作成します。

  • 追加操作と削除操作を検証する単体テスト
  • ホームページのウィジェットとお気に入りページのウィジェットのテスト
  • 統合テストを使用したアプリ全体の UI とパフォーマンスのテスト

Android で動作中のアプリの GIF

この Codelab で学びたいことは次のどれですか?

このトピックは初めてなので、簡単に概要を知りたい。 このトピックについてある程度は知っているが、復習したい。プロジェクトで使用するサンプルコードを確認したい。 特定の項目に関する説明を確認したい。

2. Flutter の開発環境をセットアップする

このラボを完了するには、Flutter SDKエディタの 2 つのソフトウェアが必要です。

この Codelab は、次のいずれかのデバイスを使って実行できます。

  • パソコンに接続され、デベロッパー モードに設定された物理デバイス(Android または iOS
  • iOS シミュレータ(Xcode ツールのインストールが必要)
  • Android Emulator(Android Studio でセットアップが必要)
  • ブラウザ(デバッグには Chrome が必要)
  • WindowsLinuxmacOS のデスクトップ アプリケーション。開発はデプロイする予定のプラットフォームで行う必要があります。たとえば、Windows のデスクトップ アプリを開発する場合は、適切なビルドチェーンにアクセスできるように Windows で開発する必要があります。オペレーティング システム固有の要件については、docs.flutter.dev/desktop に詳しい説明があります。

3. 始めるにあたって

新しい Flutter アプリを作成し、依存関係を更新する

この Codelab では、Flutter モバイルアプリのテストを中心に説明します。テストするアプリは、ソースファイルを切り貼りするだけで、すぐに作成できます。そのほかにも、各種のテストについて説明します。

a3c16fc17be25f6c.png 初めての Flutter アプリについての手順に沿って、または次のコマンドラインで、テンプレート化された簡単な Flutter アプリを作成します。

$ flutter create testing_app

a3c16fc17be25f6c.png コマンドラインで pub 依存関係を追加します。状態管理を容易にするために、次のように provider を追加します。

$ cd testing_app
$ flutter pub add provider
Resolving dependencies...
  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)
+ nested 1.0.0
  path 1.8.2 (1.8.3 available)
+ provider 6.0.5
  test_api 0.4.16 (0.4.18 available)
Changed 2 dependencies!

デバイスまたはエミュレータで Flutter コードの自動テストを行うために、次のように integration_test を追加します。

$ flutter pub add --dev --sdk=flutter integration_test
Resolving dependencies...
+ archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ crypto 3.0.2
+ file 6.1.4
+ flutter_driver 0.0.0 from sdk flutter
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ integration_test 0.0.0 from sdk flutter
  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)
+ platform 3.1.0
+ process 4.2.4
+ sync_http 0.3.1
  test_api 0.4.16 (0.4.18 available)
+ typed_data 1.3.1
+ vm_service 9.4.0 (11.0.1 available)
+ webdriver 3.0.1 (3.0.2 available)
Changed 12 dependencies!

実際のデバイスとエミュレータで動作する Flutter アプリケーションをテストするための高度な API のために、次のように flutter_driver を追加します。

$ flutter pub add --dev --sdk=flutter flutter_driver
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_api 0.4.16 (0.4.18 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!

一般的なテストツールのために、次のように test を追加します。

$ flutter pub add --dev test
Resolving dependencies...
+ _fe_analyzer_shared 52.0.0
+ analyzer 5.4.0
  archive 3.3.2 (3.3.6 available)
+ args 2.3.2
  collection 1.17.0 (1.17.1 available)
+ convert 3.1.1
+ coverage 1.6.3
+ frontend_server_client 3.2.0
+ glob 2.1.1
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ io 1.0.4
  js 0.6.5 (0.6.7 available)
+ logging 1.1.1
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ mime 1.0.4
+ node_preamble 2.0.1
+ package_config 2.1.0
  path 1.8.2 (1.8.3 available)
+ pool 1.5.1
+ pub_semver 2.1.3
+ shelf 1.4.0
+ shelf_packages_handler 3.0.1
+ shelf_static 1.1.1
+ shelf_web_socket 1.0.3
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.11
+ 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)
+ watcher 1.0.2
+ web_socket_channel 2.3.0
  webdriver 3.0.1 (3.0.2 available)
+ webkit_inspection_protocol 1.2.0
+ yaml 3.1.1
Changed 28 dependencies!

アプリ ナビゲーションの処理のために、次のように go_router を追加します。

$ flutter pub add go_router
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 6.0.4
  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)
Changed 2 dependencies!

以下の依存関係が pubspec.yaml に追加されます。

dependencies 配下:

dependencies:
  provider: ^6.0.5
  go_router: ^6.0.4

dev_dependencies 配下:

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  test: ^1.22.0

a3c16fc17be25f6c.png 任意のコードエディタでプロジェクトを開いてアプリを実行するか、次のようにコマンドラインで実行します。

$ flutter run

4. アプリを作成する

次に、アプリを作成して、テストできるようにします。アプリには次のファイルが含まれています。

  • lib/models/favorites.dart - お気に入りリストのモデルクラスを作成します。
  • lib/screens/favorites.dart - お気に入りリストのレイアウトを作成します。
  • lib/screens/home.dart - 項目のリストを作成します。
  • lib/main.dart - アプリを起動するメインファイルです。

まず lib/models/favorites.dartFavorites モデルを作成する

a3c16fc17be25f6c.png lib ディレクトリに 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.png lib ディレクトリに 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.png lib/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(
          primarySwatch: Colors.blue,
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

これでアプリは完成しましたが、テストされていません。

a3c16fc17be25f6c.png アプリを実行します。次のスクリーンショットのようになります。

b74f843e42a28b0f.png

アプリに項目のリストが表示されます。いずれかの行にあるハート型のアイコンをタップして、ハートを塗りつぶされた状態にし、その項目をお気に入りリストに追加します。AppBar の [Favorites] ボタンをクリックすると、お気に入りのリストを含む、2 番目の画面が表示されます。

これで、アプリのテスト準備が整いました。次のステップでアプリのテストを始めます。

5. プロバイダの単体テスト

favorites モデルの単体テストから始めます。単体テストとは、どんなものでしょうか。単体テストでは、ソフトウェアの各単位(関数やオブジェクト、ウィジェット)で目的のタスクが正しく実行されることを確認します。

Flutter アプリ内のすべてのテストファイル(統合テストを除く)は test ディレクトリに配置されます。

test/widget_test.dart を削除する

a3c16fc17be25f6c.png テストを開始する前に、widget_test.dart ファイルを削除します。これから独自のテストファイルを追加します。

新しいテストファイルを作成する

まず、Favorites モデル内の add() メソッドのテストとして、新しい項目がリストに追加され、リストの変更が反映されていることを確認します。慣例として、test ディレクトリのディレクトリ構造は lib ディレクトリと同じにして、Dart ファイルの名前は同じものに _test を付加します。

a3c16fc17be25f6c.png test ディレクトリに 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 テスト フレームワークでは、グループ内で互いに関連した類似のテストをまとめることができます。1 つのテストファイルに複数のグループを含めると、lib ディレクトリ内の対応するファイルの別々の部分をテストできます。

test() メソッドは、位置に意味のある 2 つのパラメータを取ります。テストの 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 の画面を別々にテストします。

ウィジェット テストでは、test() 関数の代わりに testWidget() 関数を使用します。test() 関数と同様に、testWidget() 関数は description,callback の 2 つのパラメータを取りますが、コールバックは引数として WidgetTester を取ります。

ウィジェット テストでは TestFlutterWidgetsBinding を使用します。このクラスは、実行中のアプリと同じリソース(画面サイズ、アニメーションのスケジュールに関する情報など)を提供しますが、実際のアプリは使用しません。代わりに、仮想環境を使用してウィジェットをインスタンス化し、結果をテストします。pumpWidget は、アプリケーションで行うのと同じように、特定のウィジェットを用意して測定するようにフレームワークに指示することで、この処理を開始します。

ウィジェット テスト フレームワークには、ウィジェットを探すためのファインダー(text()byType()byIcon(). など)や、結果を確認するためのマッチャーが用意されています。

まず、HomePage ウィジェットをテストします。

新しいテストファイルを作成する

最初のテストでは、HomePage のスクロールが正しく動作するかどうかを確認します。

a3c16fc17be25f6c.png test ディレクトリに新しいファイルを作成し、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.png 次のコード スニペットを home_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 Driver でアプリのパフォーマンスをテストする

パフォーマンス テストを記述する

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() 関数では、統合テストドライバが初期化済みかどうかを確認し、必要に応じて初期化をやり直します。アニメーションのあるコードをテストする場合は、framePolicyfullyLive に設定することをおすすめします。

このテストでは、項目のリストを高速に最後までスクロールしてから、スクロールで元に戻します。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.

テストが正常に完了した後には、プロジェクトのルートにある build ディレクトリに次の 2 つのファイルがあります。

  1. scrolling_summary.timeline_summary.json には、サマリーが入っています。テキスト エディタで開いて、含まれている情報を確認してください。
  2. scrolling_summary.timeline.json 完全なタイムライン データが入っています。

統合テストの詳細については、以下にアクセスしてください。

9. 演習の完了

この Codelab を修了し、Flutter アプリをテストするためのさまざまな方法を学びました。

学習した内容

  • 単体テストを使用してプロバイダをテストする方法
  • ウィジェット テスト フレームワークを使用してウィジェットをテストする方法
  • 統合テストを使用してアプリの UI をテストする方法
  • 統合テストを使用してアプリの性能をテストする方法

Flutter でのテストについて詳しくは、以下をご覧ください。