- Flutterの3種類のテストの実装方法
- Flutterの3種類のテストをGithub Actionsで自動化する方法
Flutterのテストはユニット・ウィジェット・インテグレーションの3種類
Flutterには下表に示すような3種類のテストがあり、内容は以下のようになっています。
テストの種類 | 内容 |
---|---|
ユニットテスト | Flutterの機能ではないDartオンリーな部分のテストを行う。 UIとは切り離された部分のテストである。 |
ウィジェットテスト | FlutterのWidgetを操作しながらテストを行う。 ユニットテストのWidget版とイメージしてもらうと分かりやすい。 |
インテグレーションテスト | アプリ全体の統合的なテストを行う。他のテストとは異なり、 実際にエミュレータを立ち上げ、事前に設定しておいた操作が行われる。 |
本記事では、これらのテストを実装し、Github Actionsでテスト自動化するところまで紹介します。
テスト対象のアプリを簡単に紹介する
まず、テストを実装する対象のアプリの画面を以下に示す。
Home画面 アプリについて画面
仕様は以下のような感じになっており、いかにもテストのために作られた仕様である。
- 数字をタップすると、その数が計算結果に加算される
- 計算結果が偶数か奇数かを表示する
- 右下のボタンをタップすると、計算結果を0にリセットする
- 右上のボタンをタップすると、アプリについて画面に遷移する
ユニットテストを実装する
本記事では、数字をタップしたら行われる、足し算を行う関数についてユニットテストを実装しました。
testプラグインを導入する
ユニットテストを行うためには、「flutter_test」ではなく 「test」パッケージを導入する必要があります。
dependencies:
test: ^1.15.4
ユニットテスト用のDartファイルを作成する
プロジェクトルート>testフォルダ配下に任意のファイル名のDartファイルを作成します。
そのDartファイルに、以下のように先ほど導入してtestパッケージをインポートして、テストを書いていきます。
import 'package:flutter_ci_test/calc_model.dart';
import 'package:test/test.dart';
// これとは違うので注意!! import 'package:flutter_test/flutter_test.dart';
void main() {
test('Calculation.add() test', () {
// 0+0は0なことを確認する
expect(Calculation.add(0, 0), 0);
// 1+1は2なことを確認する
expect(Calculation.add(1, 1), 2);
// 負の数にも対応していることを確認する
expect(Calculation.add(-4, 2), -2);
});
}
main()関数の中に、『test(‘テスト名’ () { expect(hoge, hoge) });』の形でテストを書いていきます。
ユニットテストを実行する
ターミナルで以下のコマンドを叩くだけでOKです。
flutter test
ウィジェットテストを実装する
本記事では、偶数/奇数の表示が正しいことと、リフレッシュボタンをタップすることで計算結果が0になることを確認するウィジェットテストを実装しました。
「flutter_test」パッケージが導入されているか確認する
pubspec.yamlではデフォルトで記載されているはずですが、一応確認しておきます。
dev_dependencies:
flutter_test:
sdk: flutter
ウィジェットテスト用のDartファイルを作成する
ユニットテストと同じく、プロジェクトルート>testフォルダ配下に任意のファイル名のDartファイルを作成します。
今回は、デフォルトで作成されていた「widget_test.dart」ファイルを直接編集しました。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_ci_test/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// ウィジェットを立ち上げる
await tester.pumpWidget(MyApp());
/// 最初は、「計算結果:0」と表示されていることを確認する
expect(find.text('計算結果:0'), findsOneWidget);
/// 計算結果が偶数の時は「偶数」、奇数の時は「奇数」と表示されていることを確認する
// (同時に各ボタンが動作することも確認する)
await tester.tap(find.byKey(Key('1')));
await tester.pump();
expect(find.text('奇数'), findsOneWidget); // この時点で計算結果:1 なので奇数
await tester.tap(find.byKey(Key('2')));
await tester.pump();
expect(find.text('奇数'), findsOneWidget); // この時点で計算結果:3 なので奇数
await tester.tap(find.byKey(Key('3')));
await tester.pump();
expect(find.text('偶数'), findsOneWidget); // この時点で計算結果:6 なので偶数
/// FloatingActionボタンを押すと、計算結果が、0になることを確認する
await tester.tap(find.byIcon(Icons.refresh));
await tester.pump();
expect(find.text('計算結果:0'), findsOneWidget);
});
}
ウィジェットテストでは、WidgetTesterを使って、仮想的に操作を行い、テストを実行しています。
また、『await tester.tap(find.byKey(Key(‘1’)));』のように、Keyを使って、タップするウィジェットを探していますが、これは事前にタップしたいウィジェットにKeyを仕込んでおく必要があります。
今回の場合は、アプリ側のコードで以下のようにKeyを仕込んであります。
Widget _numberButton(int number) {
return InkWell(
hoverColor: Colors.orangeAccent,
key: Key(number.toString()), // ★★★ここでKeyを設定している★★★
onTap: () {
setState(() {
calcResult = Calculation.add(calcResult, number);
});
},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
border: Border.all(),
),
child: Center(child: Text(number.toString(), style: TextStyle(fontSize: 50),)),
),
);
}
ウィジェットテストを実行する
ユニットテストと同じく、以下のコマンドを叩くだけでOKです。
flutter test
インテグレーションテストを実装する
本記事では、以下の操作を仮想的に行い、インテグレーションテストとしています。
- 起動直後の画面をスクリーンショットする
- アプリについて画面に遷移する
- 表示されているアプリのバージョンが期待値通りか確認する
- アプリについて画面をスクリーンショットする
- 起動直後の画面に戻る
flutter_driverパッケージを導入する
pubspec.yamlに以下のように記載して、flutter_driverを導入する
dev_dependencies:
flutter_test:
sdk: flutter
flutter_driver: // ★追加
sdk: flutter // ★追加
インテグレーションテスト用のDartファイルを作成する
プロジェクトルートに「test_driver」フォルダを作成し、そこに「test.dart」と「app_test.dart」を作成します。
今回はスクリーンショットを保存するために、「screenshots」フォルダも作成しました。

まず、app.dartの中身は以下のように記載します。
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_ci_test/main.dart' as app;
void main() {
enableFlutterDriverExtension(); // Flutter拡張機能を有効にする
app.main();
}
次に、app_test.dartの中で、実際のテストを記述していきます。
import 'dart:io';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('Driver Test', () {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
if (driver != null) {
await driver.close();
}
});
/// スクリーンショットを取る関数
Future<void> _takeScreenShot(String filename) async {
await driver.waitUntilNoTransientCallbacks();
final pixels = await driver.screenshot();
final file = File('./test_driver/screenshots/$filename.png');
await file.writeAsBytes(pixels);
print('wrote $file');
}
test('起動直後の画面', () async {
final health = await driver.checkHealth();
print(health.status);
await _takeScreenShot('起動直後の画面');
});
test('アプリについて画面へ遷移して戻ってくる', () async {
final health = await driver.checkHealth();
print(health.status);
await driver.tap(find.byValueKey('toAppAbout')); // アプリについて画面に遷移する
expect(await driver.getText(find.byValueKey('appVersion')), 'アプリバージョン:1.0.0');
await _takeScreenShot('アプリについて画面');
await driver.tap(find.byTooltip('Back')); // 元の画面に戻る
});
});
}
setUpAll()でテスト開始前にアプリに接続し、tearDownAll()でテスト終了後にアプリから切断しています。その他は、他のテストと大差ない書き方です。※FlutterDriverの使い方には慣れが必要。
インテグレーションテストを実行する
シミュレータを起動した後に、ターミナルで以下のコマンドを叩くだけでOKです。
flutter drive --target=test_driver/app.dart
3種類のテストをGithub Actionsで自動化する
ここからは、Github リポジトリにプッシュした時に、自動ですべてのテストが走るように実装してみます。
まず、ワークフロー(ymlファイル)を用意する
プロジェクトルート>.github>workflowsフォルダに○○○.yamlファイルを作成します。
on
句ではgithubにpushしたときにActionsが走ることを指定しています。
name: flutter_test
on:
push:
workflow_dispatch:
jobs:
※jobs配下にテスト実行するスクリプトを書いていきます。
ユニット・ウィジェットテストを自動化する
Github ActionsでFlutterの環境構築をしてくれる、Flutter action
を使用させていただきました。
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.2.0'
channel: 'stable'
- run: flutter pub get
- run: flutter test
Flutterの環境構築をしたあとに、pub get
して、テストを走らせているだけなので、分かりやすいと思います。
インテグレーションテストを自動化する
以下のコードで実現できます。
jobs:
drive_test:
runs-on: macos-latest
strategy:
matrix:
device:
- "iPhone 8 (14.4)"
- "iPhone 11 Pro Max (14.4)"
steps:
- name: "List all simulators"
run: "xcrun instruments -s"
- name: "Start Simulator"
run: |
UDID=$(
xcrun instruments -s |
awk \
-F ' *[][]' \
-v 'device=${{ matrix.device }}' \
'$1 == device { print $2 }'
)
xcrun simctl boot "${UDID:?No Simulator with this name found}"
- uses: actions/checkout@v1
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.2.0'
channel: 'stable'
- name: "Run Flutter Driver tests"
run: "flutter drive --target=test_driver/app.dart"
strategy
とmatrix
で複数のデバイスでテストするようにしていますが、一つでも構いません。
name: "Start Simulator"
部分では、matrix
で指定したOSに一致するものをxcrun instruments
により起動しています。
その後は、ローカルと同様に、インテグレーションテストを実行するコマンドを叩いているだけです。
おわりに
Github Actionsはプライベートリポジトリの場合、使用するOS、使用時間により従量課金制になっていますので、ご注意ください。
また、インテグレーションテストはテスト実行に時間がかかるので、特に注意が必要です。
なのでまずは、サンプルアプリを作って、パブリックリポジトリで遊んでみるのをおすすめします。
ソースコードはこちら
コメント