内容
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
出来上がったコード