Slide 1

Slide 1 text

FutureCon 2022 An Encouragement of Flutter Golden Test

Slide 2

Slide 2 text

ABOUT ME 武田 大輝(Takeda Hiroki) Technology Innovation Group Backend : Golang Frontend : Angular, Vue, Ionic, Flutter datake914 Software Design 2022年8月号 (2022年7月15日発売) 第4章 「OpenAPIを使ったWEB API開発の実際」を 執筆させていただきました。 2

Slide 3

Slide 3 text

CONTENTS 1. Introduction: What is Golden Test? 2. How to Create Golden Test? 3. Golden Test with Golden Toolkit 4. Integrate with Widget Tree Test 5. Integrate with Riverpod 6. Summary 3

Slide 4

Slide 4 text

1. Introduction: What is Golden Test?

Slide 5

Slide 5 text

Types of Testing in Flutter cf. https://docs.flutter.dev/testing Unit Test 単一の関数、メソッド単位 でのホワイトボックステスト プログラムの内部構造に着目し、 様々な条件下でロジックが 確からしいことを確認する Low Widget Test Widget単位での コンポーネントテスト WidgetのUIが期待通りに 反応し、表示されることを確認する Higher Integration Test アプリケーション単位での 統合テスト 全てのWidgetとサービスが 期待通りに連携して機能することを 確認する Highest 信頼性 メンテナンスコスト 依存関係 実行速度 Low Higher Highest Few More Most Quick Quick Slow 5

Slide 6

Slide 6 text

What is Golden Test? Golden TestはWidget Testの一種 Goldenとは・・・ 特定のウィジェットや状態の真となるレンダリングと見なされるマスタ画像 > The term golden file refers to a master image that is considered the true rendering of a given widget, state, application, or > other visual representation you have chosen to capture. cf. https://api.flutter.dev/flutter/flutter_test/matchesGoldenFile.html Widget Tree Test (通常のWidget Test) Widget Treeを探索評価 Widget Test Golden Test Widgetのスクリーンショットを ピクセルレベルで比較 6

Slide 7

Slide 7 text

Golden Test Flow Goldenの生成 flutter test –-update-golden ソースコードの修正 テスト(イメージの生成 & 比較) flutter test 事前準備 テスト イメージの比較 一致する場合:テスト成功 一致しない場合: テスト失敗 7

Slide 8

Slide 8 text

2. How to Create Golden Test?

Slide 9

Slide 9 text

Sample Widget 9 テスト対象は標準サンプルのカウンターアプリ

Slide 10

Slide 10 text

Pure Golden Test - Write Test Code - 10

Slide 11

Slide 11 text

Pure Golden Test - Write Test Code - 11 Load Fonts デフォルトだと Ahem と呼ばれる正方形フォントが使用されるため、 正確なゴールデンを生成するためにカスタムフォントをロードする。

Slide 12

Slide 12 text

Pure Golden Test - Write Test Code - 12 tester.pumpWidget 引数に指定されたWidgetをレンダリングする

Slide 13

Slide 13 text

Pure Golden Test - Write Test Code - 13 matchesGoldenFile Widgetのスクリーンショットが、引数で指定された Goldenと一致することを検証する非同期のMatcher

Slide 14

Slide 14 text

Pure Golden Test - Generate a Golden - matchesGoldenFile の引数に 指定したパスにGoldenを生成 goldens/homepage.png 14

Slide 15

Slide 15 text

Pure Golden Test - Execute Test - failures/ homepage_masterImage.png failures/ homepage_testImage.png failures/ homepage_isolatedDiff.png failures/ homepage_maskedDiff.png 成功時 失敗時 15

Slide 16

Slide 16 text

複数のテストシナリオを単一のGoldenにまとめて出力したい 実際のデバイスのサイズをベースとしてGoldenを出力したい 16

Slide 17

Slide 17 text

3. Golden Test with Golden Toolkit

Slide 18

Slide 18 text

Golden Toolkit eBayが開発している、Golden Testを構築するためのライブラリ 主要な機能 • GoldenBuilder Widgetに対する様々なテストシナリオを定義し、全てのテストシナリオ のスクリーンショットを含む単一のGoldenイメージを生成することが できる。 • DeviceBuilder 事前に定義したデバイスに応じて、デバイスのサイズ別に各シナリオの スクリーンショットを単一のGoldenイメージに含めることができる。 <各種ユーティリティの提供> • Font Loader • Material Appのラッパー • matchedGoldenFileのラッパー などなど 18

Slide 19

Slide 19 text

Golden Test with Golden Toolkit - Set Up - failuresディレクトリをGit管理対象外にする .gitignore テスト実行時の設定ファイルに "golden" タグを追加する dart_test.yaml Golden Toolkitによるテスト実行時に、各テストにはデフォルトで "golden" タグが付与されるため、 プロジェクト全体としてこのタグを既知のタグとして定義しておかないとテスト実行時に警告がでる。 19

Slide 20

Slide 20 text

Golden Test with Golden Toolkit - Set Up - 共通的にフォントを読み込む Flutterは, テスト対象のファイルを起点として、上位階層のディレクトリに flutter_test_config.dart が存在するか スキャンを行い、 ファイルが見つかった場合は testExecutable() を実行する。 cf. https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html flutter_test_config.dart 20

Slide 21

Slide 21 text

Golden Test with Golden Toolkit - Writing Test Code - 21

Slide 22

Slide 22 text

Golden Test with Golden Toolkit - Writing Test Code - testGoldens Flutter標準の testWidgets() のラッパーメソッド このメソッドを利用することで 各テストに “golden” タグが付与さ れ、 flutter test --tags golden でGolden Testのみ を実行することが可能になる。 後述する screenMatchesGolden() メソッドなどは、 このメソッドの内部で呼ばれていないとエラーとなるため、利用必須。 22

Slide 23

Slide 23 text

Golden Test with Golden Toolkit - Writing Test Code - DeviceBuilder 複数のデバイスや複数のテストシナリオを定義するためのビルダ 後述する overrideDevicesForAllScenarios() メソッドや addScenario() メソッドを持つ。 23

Slide 24

Slide 24 text

Golden Test with Golden Toolkit - Writing Test Code - overrideDevicesForAllScenarios テスト対象のWidgetを描画するデバイスを定義するメソッド あらかじめ定義された Device.tabletLandscape や Device.iphone11 などが利用できるほか、Device クラスを生 成して独自のデバイスサイズを定義することが可能。 24

Slide 25

Slide 25 text

Golden Test with Golden Toolkit - Writing Test Code - addScenario テストシナリオを定義するメソッド テスト対象となるWidgetを引数で指定する。 ここでは、Flutter標準サンプルのカウンターページが、カウント値の 初期値を受け取れるよう修正し、初期値を受け取らないケース、受 け取るケースの2シナリオを定義している。 25

Slide 26

Slide 26 text

Golden Test with Golden Toolkit - Writing Test Code - tester.pumpDeviceBuilder DeviceBuilder を受け取ってWidgetを描画するための WidgetTester の拡張メソッド 引数で WidgetWrapper を指定することでテスト対象の Widgetを任意のWidgetでラップすることが可能。 例えば、テスト対象のWidgetが特定のWidgetの配下で描画さ れることを前提としている場合などに指定が必要となる。 何も指定しない場合はデフォルトで、MaterialApp Widgetで ラップされる。 26

Slide 27

Slide 27 text

Golden Test with Golden Toolkit - Writing Test Code - screenMatchesGolden Flutter標準の matchesGoldenFile のラッパーメソッド 第一引数に tester 第二引数に Goldenのファイル名を指定する。 27

Slide 28

Slide 28 text

Golden Test with Golden Toolkit - Generate a Golden - goldens/homepage.png 定義したデバイス、テストシナリオ別の スクリーンショットが単一のGoldenとして出力 28

Slide 29

Slide 29 text

Golden Test with Golden Toolkit - TIPs - VSCodeのコンテキストメニューから特定のテストのGoldenを生成する .vscode/launch.json 29

Slide 30

Slide 30 text

Golden Test with Golden Toolkit - TIPs - Goldenの生成環境を統一する tools/docker-compose.yaml OSやFlutter/Dartのバージョンによって生成されるGoldenが微妙に異なるため、開発者の環境が分かれる場合は Dockerなどを利用して環境差異を吸収する。 Golden生成 30

Slide 31

Slide 31 text

4. Integrate with Widget Tree Test

Slide 32

Slide 32 text

Integrate with Widget Tree Test Golden TestはWidget Tree Testのかわりとなるものではなく、 それぞれ異なる目的のために補い合うもの Widget Treeの状態が 期待通りであることを確認する 特定のアクションを実施したときに Widgetが存在するかどうかの確認、など Widgetの視覚的な状態が 期待通りであることを確認する 文字列の折り返しなど特定のデバイスサイズを 前提とした描画の確認、など Widget Tree Test (通常のWidget Test) Widget Treeを探索評価 Golden Test Widgetのスクリーンショットを ピクセルレベルで比較 32

Slide 33

Slide 33 text

Integrate with Widget Tree Test - Integrate with the Widget Tree Test - onCreate 各シナリオのテスト対象のWidgetが作成されたときのHookを定 義することができる。Widget Tree Testはここに記述する。 Widgetを探索する場合は、必ず引数の key 配下のWidgetを 対象とする必要がある。 key は <シナリオ名 + デバイス名> となる。 Widget の描画イメージ 全シナリオ、全デバイスのWidgetが存在するため、keyで絞込を行わないと、 適切にFindできない Parent Widget Senario1 – Device① Widget Senario2 – Device① Widget Senario1 – Device② Widget Senario2 – Device② Widget 33

Slide 34

Slide 34 text

Integrate with Widget Tree Test - Troubleshooting - Expected: exactly one matching node in the widget tree 先述の通り、各シナリオ、各デバイスごとのWidgetは1Widget内に描画されるため、 findsOneWidget などで対象のWidgetがユニークであることを確認しようとした際に、上記エラーが発生することがある。 • シナリオ名が重複していないか • Widgetを探索する際に onCreate の中で key による絞込を行っているか を確認する。 pumpAndSettle timed out tester.pumpAndSettle() メソッドを利用して、描画フレームがなくなるまで待機している場合に発生することがある。 ローディングアニメーションが表示しつづけていたり、テキストのカーソルが点滅していたりなど、 描画フレームが常に発生するようになっていないか確認する。 34

Slide 35

Slide 35 text

5. Integrate with Riverpod

Slide 36

Slide 36 text

Integrate with Riverpod - Background - • 通常のアプリケーションではAPI通信など アプリケーション外部に対するデータアクセスが発生する。 • Golden Test(Widget Test)時には、 これらのデータアクセスレイヤはモック化するのが定石となる。 • 今回は状態管理ライブラリとしてRiverpodを利用する前提で、 モック化含めどのようにGolden Testに組み込むか説明を行う。 36

Slide 37

Slide 37 text

Integrate with Riverpod - Sample Counter App - Flutterのカウンターアプリを修正し、API通信をシミュレート 初期描画時にサーバから カウント初期値を取得 37

Slide 38

Slide 38 text

Integrate with Riverpod - Application Architecture - HomePage (UI) HomePageState (State) HomePageStateNotifier (Notifier) CountRepository (Repository) HomePageStateNotifierProvider (Provider) 38

Slide 39

Slide 39 text

Integrate with Riverpod - Writing Application Code - repository/count_repository.dart 39

Slide 40

Slide 40 text

Integrate with Riverpod - Writing Application Code - home_page_state_notifier.dart home_page_state.dart 40

Slide 41

Slide 41 text

Integrate with Riverpod - Writing Application Code - home_page_state_notifier_provider.dart 41

Slide 42

Slide 42 text

Integrate with Riverpod - Writing Application Code - home_page.dart ref.watch Stateを監視し、変更があった場合は、 Widgetをリビルドする。 ここでは、カウンターの値を監視し、Text Widget に描画している。 42

Slide 43

Slide 43 text

Integrate with Riverpod - Writing Application Code - home_page.dart home_page.dart FutureBuilder 非同期処理の結果を待ってからWidgetを ビルドしてくれるWidget ここでは HomePageStateNotideir の initialize() を呼び出し、初期カウント 値をセットしている。 initialize() 実行中は サークルインジケータが表示される。 43

Slide 44

Slide 44 text

Integrate with Riverpod - Writing Test Code - 44

Slide 45

Slide 45 text

Integrate with Riverpod - Writing Test Code - 45 @GenerateMocks Mockitoを利用してリポジトリのモックを生成し、 モックの振る舞いを定義

Slide 46

Slide 46 text

Integrate with Riverpod - Writing Test Code - 46 ProviderScope の overrides を利用して MockをInject

Slide 47

Slide 47 text

Integrate with Riverpod - Writing Test Code - 課題: DeviceBuilder#addScenario() ではMockのInjectがツライ home_page_test.dart device_builder.dart builderの外側でシナリオ数分Mockを定義しなければならず、 可読性が著しく低下する。 47

Slide 48

Slide 48 text

Integrate with Riverpod - Writing Test Code - 対応: DeviceBuilder を拡張して DeviceScenario のBuilderを追加できるようにする home_page_test.dart custom_device_builder.dart 48

Slide 49

Slide 49 text

Integrate with Riverpod - Writing Test Code - 課題: デバイスが複数定義されている場合に、同一のWidgetが使いまわされる。 device_builder.dart Parent Widget Senario1 – Device① Widget Senario2 – Device① Widget Senario1 – Device② Widget Senario2 – Device② Widget ProviderScope や Mock が同一シナリオ内で共有されるため、 onCreate における verify でエラーとなる可能性がある。 例えば verify(mockCountRepository.get()).callCount は 「1」となって欲しいが、デバイス数分カウントされてしまう。 Mock Scope 49

Slide 50

Slide 50 text

Integrate with Riverpod - Writing Test Code - 対応: addScenarioBuilder にてデバイス別にシナリオをセットするようにする custom_device_builder.dart custom_device_builder.dart 各デバイスごとに overrideDevicesForAllScenarios を 行うことで、シナリオ別、デバイス別にシナリオインスタンスを保持する。 50

Slide 51

Slide 51 text

Integrate with Riverpod - Completed Test Code - 51

Slide 52

Slide 52 text

Integrate with Riverpod - Completed Test Code - addScenarioBuilder を利用してシナリオ別に Mockを定義 52

Slide 53

Slide 53 text

Integrate with Riverpod - Completed Test Code - ProviderScope の overrides を利用して MockをInject 53

Slide 54

Slide 54 text

Integrate with Riverpod - Completed Test Code - Widgetの描画後に Mockの検証及びWidget Treeの検証 54

Slide 55

Slide 55 text

Integrate with Riverpod - Completed Test Code - Goldenの検証 55

Slide 56

Slide 56 text

6. Summary

Slide 57

Slide 57 text

Summary • Golden TestはWidget Testの一種で、スクリーンショットを比較するテスト • Golden Toolkitを使うことで、よりリッチにGoldenを生成可能(使わなくてもできる) • 実際にはGolden Testに Widget Tree Test や Mock の検証なども統合する形で、 テストコードを記述していくのがよい。 資料中のコードの全量は下記のリポジトリにあります。 https://github.com/datake914/flutter-golden-test-sample 57

Slide 58

Slide 58 text

Enjoy Golden Test! 58