内容
github ワークフローで実現したいこと
- code formatできてるかをチェック
 - flutter analyzeで静的分析
 - dependabotでpackage updateの自動化
 - test
- unit test
 - widget test
 - golden test
 - code coverageチェック
 
 
まず、サンプルapp作成
templateを使ってアプリを作成するツールを使います。
基本的なものが整備されるので、便利です〜
- Blocで実現した定番のcounterアプリ
 - Unit, widget testコード
 - github workflow
 
アプリを作成
dart pub global activate very_good_cli
very_good create learn_flutter_test
very_good packages get
テストコードを実行
サンプルのテストコードがあるので、早速実行して見ます
commandで実行する方法
fvm flutter test --coverage
# coverage/lcov.infoにcoverageが生成される
生成されたcoverageファイルををhtmlに変換して読めるようにする
brew install lcov
genhtml coverage/lcov.info -o coverage/html
Android studioで実行する方法
testフォルダを右クリックして、メニューRun 'tests in test with Coverage'をクリックすると、
IDE上の左側のfile browserでファイル毎のCoverageが確認できる

Gloden test
どんなテスト?
UIの期待画像を事前に生成しておいて、テストで期待画像を比較して、差分があるかを確認するUI testの一種です。
package golden_toolkitを使うので、以下のようにdependencyを指定します
dev_dependencies:
  golden_toolkit: ^0.14.0
ファイルtest/flutter_test_config.dartを作成して、以下のようなコードにする
- これはテストが実行する前に実行するファイル
 
import 'dart:async';
import 'package:golden_toolkit/golden_toolkit.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
  await loadAppFonts(); // localしないと、screenshotで文字が表示できない問題が発生します
  await testMain();
}
テストコード書く
test/counter_page_golen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:learn_flutter_test/app/app.dart';
void main() {
  testGoldens('CounterPage init', (tester) async {
    await tester.pumpWidgetBuilder(const App());
    await screenMatchesGolden(tester, 'CounterPage_init');
  });
  
  testGoldens('CounterPage click', (tester) async {
    await tester.pumpWidgetBuilder(const App());
    await tester.tap(find.byIcon(Icons.add));
    await screenMatchesGolden(tester, 'CounterPage_click-1-times');
    await tester.tap(find.byIcon(Icons.add));
    await screenMatchesGolden(tester, 'CounterPage_click-2-times');
  });
}
glodenファイル(期待してるUI画像)を生成
fvm flutter test --update-goldens
testを実行
 fvm flutter test test/counter_page_golen_test.dart 
  void increment() => emit(state + 1); //修正前
  void increment() => emit(state + 10); //修正後
テスト結果出力の抜粋です。結果から見ると、click1回、2回した後の画像が期待結果と異なってます。
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
Golden "goldens/CounterPage_click-1-times.png": Pixel test failed, 0.90% diff detected.
....
The following assertion was thrown while running async test code:
Golden "goldens/CounterPage_click-2-times.png": Pixel test failed, 0.92% diff detected.
....
00:05 +1 -1: Some tests failed. 
実際の画像の差分はfailuresフォルダに出力した画像から確認できます
- _testImage.png ⇨ 実際出力した画像
 - _masterImage.png ⇨ 期待画像
 - _maskedDiff.png ⇨ 差分の画像
 - _isolatedDiff.png  ⇨ 差分の箇所だけの画像
 
responstive UIをテストしたい場合
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:learn_flutter_test/app/app.dart';
void main() {
  testGoldens('CounterPage click', (tester) async {
    final builder = DeviceBuilder()
      ..overrideDevicesForAllScenarios(devices: [
        Device.phone,
        Device.iphone11,
        const Device(name: 'ipad pro', size: Size(1366, 1024))
      ]) // テストしたい端末かサイズを指定
      ..addScenario(
        widget: const App(),
        name: 'init',
      )
      ..addScenario(
        widget: const App(),
        name: 'tap add button 1 times',
        onCreate: (scenarioWidgetKey) async {
          final finder = find.descendant(
            of: find.byKey(scenarioWidgetKey),
            matching: find.byIcon(Icons.add),
          );
          await tester.tap(finder);
        },
      );
    await tester.pumpDeviceBuilder(builder);
    await screenMatchesGolden(tester, 'counter-page');
  });
}
Integration test
pubspec.yamlにintegration_test packageを指定する
dev_dependencies:
  integration_test:
    sdk: flutter
テストdriverを指定する
test_driver/integration_test.dart
import 'package:integration_test/integration_test_driver_extended.dart';
Future<void> main() async => integrationDriver();
テストコードを書く
integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:learn_flutter_test/main_production.dart' as app;
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  testWidgets('counter', (tester) async {
    debugPrint('---test start--');
    app.main();
    await tester.pumpAndSettle();
    final counter = find.byKey(const Key('count')).evaluate().single.widget as Text;
    expect(counter.data, '0');
    final addButton = find.byIcon(Icons.add);
    await tester.tap(addButton);
    await tester.pumpAndSettle();
    expect(counter.data, '1');
    await tester.tap(addButton);
    await tester.pumpAndSettle();
    expect(counter.data, '2');
    debugPrint('---test end--');
  });
}
テストを実行
まずはchromedriverをinstallしておきます。
いろんなtarget deviceでテストを実行する
chromedriver --port=4444
# chromeで実行する
fvm flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d chrome
# no-headlessで実行する
fvm flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d web-server --no-headless
# headlessで実行する
fvm flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d web-server --headless
Target deviceにより挙動が少し違います。
- web-server: 
- debugPrintでlogを出力できない
 
 - chrome:
- debugPrintでlogを出力できる
 - 一部のexpectが失敗しても、最終結果が
All tests passed.になる問題がある。このコメントを参考して、対応できる 
 
github workflowの設定
注意点: Gloden testの画像生成はOSにより差分があるので、worflow上のOSがlocalと同じにしないと、テストが失敗する可能性があります。
name: learn_flutter_test
on: [pull_request, push]
jobs:
  build:
    uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
    with:
      flutter_channel: stable
      flutter_version: 3.3.8
      runs_on: macos-latest # localのOSと同じ
  integration-test:
    runs-on: macos-latest
    steps:
      - name: 📚 Git Checkout
        uses: actions/checkout@v3
      - name: Setup flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
          flutter-version: 3.3.8
          cache: true
          cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}
      - name: Run Test
        run: |
          chromedriver --port=4444 &
          flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d web-server
実行結果はここにあります。
https://github.com/qiuyin/learn_flutter_test/actions/runs/3705354235
出来上がったコード




