5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FlutterでProviderを使用しているアプリのテスト方法

Posted at

はじめに

この記事はGoogleのCodelabにて公開されている以下のチュートリアルの個人的なまとめです。
Flutterアプリをテストする方法

テスト対象のアプリ

testing_app.gif
一覧ページとお気に入りページがあり、アイコンタップでお気に入り登録と解除ができるよくあるパターンのアプリです。
お気に入りの管理にProviderを使用しています。

アプリのディレクトリ構成

testing_app/
┣ lib/
┃ ┣ models/
┃ ┃ ┗ favorites.dart
┃ ┗ screens/
┃   ┗ favorites.dart
┃   ┗ home.dart
┗ test/

中身のコードは以下を参照してください
codelabのコード

テストファイルのディレクトリ構成

プロジェクト作成時に自動生成されているtestフォルダに追加していきます。
テストファイルを追加する際は、アプリ側の構成と合わせて配置するとわかりやすいです。
今回の場合は以下の通りです。
testing_app/
┣ ...
┗ test/
 ┗ models/
 ┃  ┗ favorites_test.dar
 ┣ favorites_test.dart
 ┗ home_test.dart

テストファイルにはlib側のファイル名 + _testで命名します。
※テストランナーがテストファイルを検索する際に使用するようなので必ずつけましょう

ユニットテスト

単純な関数やメソッドに対するテストを行います。
計算処理や追加処理などが該当します。
今回はmodels/favorites.dartがテスト対象

準備

pubspec.ymlに以下を記載

pubspec.yaml
dev_dependencies:
  test: ^1.14.4
  flutter_test: ^1.0.1

実装

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

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

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

    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);
    });
  });
}

テスト内容説明

・お気に入り追加テスト
・お気に入り削除テスト

使用したメソッドなどの説明

group(description, body)
 複数の関連するテストをまとめることができます。

test(description, body)
 テスト本体です。

expect(actual, matcher)
 結果を比較します。

(CLI)テストを実行する

$ flutter test test/models/favorites_test.dart 

すべてOKだった場合の結果表示はこうなります。

00:06 +2: All tests passed!

(GUI)テストを実行する

アイコンを押下するだけです。
スクリーンショット 2021-03-28 12.13.21.png
すべてOKだった場合の結果はこうなります。
スクリーンショット 2021-03-28 12.23.39.png

ウィジェットテスト

ボタンタップでsnackbarが表示されるかなどのウィジェットの挙動をテストします。
テスト対象
 ・screens/favorites.dart
 ・screens/home.dart

実装

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: MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing if ListView shows up', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.byType(ListView), findsOneWidget);
    });

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

    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(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(Duration(seconds: 1));
      expect(find.text('Removed from favorites.'), findsOneWidget);
      expect(find.byIcon(Icons.favorite), findsNothing);
    });
  });
}

テスト内容説明

上から順に
・お気に入り一覧(ListView)が表示されているか確認
・スクロールで'Item 0'が画面外に出たか確認
・favorite_borderアイコンをタップでお気に入り追加され、snackBarに追加メッセージが表示されることと、favoriteアイコンをタップでお気に入りから削除され、snackBarに削除メッセージが表示されることを確認

使用したメソッドなどの説明

createHomeScreen()
 Providerによる状態管理を再現するために使用します。

testWidget(description, callback)
 ウィジェットテストの場合はこちらを使用します。

tester
 アプリを操作するための変数です(WidgetTest型)。
 ボタンをタップ、スクロールなどはすべてtesterで行います。
  ・pumpWidget(widget)
   テスト対象のウィジェットを準備する。
  ・fling(finder, offset, speed)
   finderで指定したウィジェットをスクロールする。
  ・tap(finder)
   finderで指定したウィジェットをタップする
  ・pumpAndSettle(duration)
   ウィジェットの描画完了待機

find
 flutter_testパッケージが提供するクラス(CommonFinders)
 ウィジェットを探すのに使用します。
 こちらも色々なメソッドが用意されています。
  ・byType(type)
   引数で渡された型に一致するウィジェットを探索し、Finder型で返す
  ・byIcon(icon)
   アイコンで探す。
  ・byText(text)
   引数のテキストと一致するテキストを保持しているウィジェットを探す。
  
・expect()の第二引数について
 第一引数のfind結果と比較する定数です
 findsNotiong ウィジェットがfindツリー上に存在しない
 findsOneWidget ウィジェットがfindツリー上に一つだけ存する

テストを実行する

※実行方法はユニットテストと同様なので割愛

インテグレーションテスト

準備

pubspec.ymlに以下を追加します

pubspec.yml
dev_dependencies:
  integration_test: ^1.0.1
  flutter_driver:
    sdk: flutter

インテグレーションテストではtesting_app/testディレクトリは使用しません。
ディレクトリとテストファイルを以下のように追加します。
testing_app/
┣ ...
┗ integration_test/ ←new
 ┣ driver.dart ←new
 ┗ app_test.dart ←new

実装

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

Future<void> main() => integrationDriver();

integrationDriver()
 flutter_driveでテストを実行するためのアダプターで、計測したパフォーマンスデータの出力なども行ってくれます。

app_test.dart
import 'package:flutter/cupertino.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 Tests', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
        as IntegrationTestWidgetsFlutterBinding;

    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

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

      final lastFinder = find.byType(ListView);

      await binding.watchPerformance(() async {
        await tester.fling(lastFinder, Offset(0, -500), 10000);
        await tester.pumpAndSettle();

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

    testWidgets('Favorites operaitons test', (tester) async {
      await tester.pumpWidget(TestingApp());

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

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

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

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

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

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

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

使用したメソッドなどの説明

ensureInitialized()
 テストドライバーの初期化確認と最初期化などを行ってくれる
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
 フレームのポンピング設定(fullyLiveの場合は意図的にポンピングしなくとも自動的にポンピングするようです)
watchPerformance()
 パフォーマンスを計測して指定した名称でjson出力してくれます。
 出力先:test_app/build/integration_response_data.json

テストを実行する

$ flutter drive --driver integration_test/driver.dart --target integration_test/app_test.dart --profile

実行結果

すべてOKだとこんな感じで出力されます。

Running "flutter pub get" in testing_app...                         4.9s
Signing iOS app for device deployment using developer identity: "Apple Development: arlezplue.7@gmail.com (5KFW69RP5Y)"
Running pod install...                                           1,341ms
Running Xcode build...                                                  
 └─Compiling, linking and signing...                         5.9s
Xcode build done.                                           11.9s
Installing and launching...                                        14.8s
VMServiceFlutterDriver: Connecting to Flutter application at http://127.0.0.1:54091/jJdzr8Y-sYM=/
VMServiceFlutterDriver: Isolate found with number: 4018486944099739
VMServiceFlutterDriver: Isolate is paused at start.
VMServiceFlutterDriver: Attempting to resume isolate
flutter: 00:00 +0: Testing App Performance Tests Scrolling test
VMServiceFlutterDriver: Connected to Flutter application.
flutter: 00:11 +1: Testing App Performance Tests Favorites operaitons test
flutter: 00:20 +2: Testing App Performance Tests (tearDownAll)
flutter: Warning: integration_test test plugin was not detected.
flutter: 00:20 +3: All tests passed!
All tests passed.

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?