この記事は全部俺 Advent Calendar 2018の12日目の記事です。
Flutterテストの種類について
ソースコードで記載するFlutterのテストには、以下のように3種類のテストがあります。
Unit Testが最も簡素なテストで、Integration Testが最も重たいテストになっています。
Unit | Widget | Integration | |
---|---|---|---|
Confidence | Low | Higher | Highest |
Maintenance cost | Low | Higher | Highest |
Dependencies | Few | More | Most |
Execution speed | Quick | Slower | Slowest |
理想的なテストでは、Unit TestとWidget Testによりコードカバレッジが担保されており、Integration Testによって重要なユースケースが担保されている状態となります。
ソースコードありのテストケースだけで、ユースケースを含めたかなりの範囲をテストすることが可能です。
一番複雑なテストであるIntegration Testでは、以下のようにエミュレータ上で実際にスクロールを行うようなテストを記載する事もできます。(わかりにくいですが、人間は何も操作せず、全自動でスクロールが行われています。)
Unit Test
ビジネスロジックに対するテストを記載します。
BLoCパターンで言うビジネスロジックももちろんこちらに記載しますが、それ以外でも単純な関数などはこちらに記載することになります。
まず、以下のようにtest
パッケージを依存関係に含めます。
dev_dependencies:
flutter_test:
sdk: flutter
test:
そして、以下のようなファイルを作成してテストを実行すると、実行結果が出力されるはずです。
import 'package:test/test.dart';
void main() {
test('my first unit test', () {
var answer = 42;
expect(answer, 42);
});
}
~/A/flutter_apptest> flutter test test/unit_test.dart
00:05 +1: All tests passed!
テストを複数記載する場合は、test()
を追加していけばOKです。
import 'package:test_api/test_api.dart';
void main() {
test('my first unit test', () {
var answer = 42;
expect(answer, 42);
});
test('Test order of operations: 12 + 3 * 4 = 24', () {
const int expression = 12 + 3 * 4;
expect(expression, equals(24));
});
}
test()
が複数記載されている場合にflutter test
を実行すると全ケース実行されますが、特定のケースのみ実行したい場合は下記のように--plain-name
をつけてテスト名を指定してあげればOKです。
~/A/flutter_apptest> flutter test --plain-name "Test order of operations: 12 + 3 * 4 = 24" test/unit_test.dart
00:02 +1: All tests passed!
~/A/flutter_apptest> flutter test test/unit_test.dart
00:02 +2: All tests passed!
Widget Test
Widget上での挙動を含めてテストします。
Seleniumを使用してヘッドレスブラウザでのテストを記載したことのある人はイメージしやすいかもしれません。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('my first widget test', (WidgetTester tester) async {
// You can use keys to locate the widget you need to test
var sliderKey = UniqueKey();
var value = 0.0;
// Tells the tester to build a UI based on the widget tree passed to it
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
);
},
),
);
expect(value, equals(0.0));
// Taps on the widget found by key
await tester.tap(find.byKey(sliderKey));
// Verifies that the widget updated the value correctly
expect(value, equals(0.5));
});
}
こちらについてもUnit Testと同様に、flutter test test/widget_test.dart
を実行するとテストが実行されます。
Unit Testとの違いは以下のとおりです。
-
flutter_test
パッケージを使用する -
testWidgets
関数を使用し、第二引数でWidgetTester
型を受け取る
await tester.pumpWidget()
pumpWidgetでWidgetの立ち上げを行い、以下のコードでvalueの値を確認しています。
expect(value, equals(0.0));
その後、以下のコードでスライダーをタップして、値が変化していることを確認しています。
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
Integration Test
日本語に直すと結合テストです。
flutter_driver
ライブラリを用いて、エミュレータ上でテストを実行します。
Integration Testでは、以下の順番でテストを実行します。
- 依存ライブラリ
flutter_driver
のインストール -
FlutterDriverExtension
機能の有効化 - エミュレータを起動してアプリケーションを起動
- テストコードを記載して実行
pubspeck.yaml
に以下のように記載します。
dev_dependencies:
flutter_driver:
sdk: flutter
Integration Testのディレクトリ構造は、Unit Testや Widget Testと違い、myapp/test/xxx_test.dart
という配置ではなく、myapp/test_driver/xxx.dart
とmyapp/test_driver/xxx_test.dart
という配置にします。
Integration Testでは2ファイル作成する必要があるので要注意です。
以下のソースコードは、flutter_galleryのテストコードをお借りしました。
flutter_galleryでスクロールを行う際のテストコードになります。
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_gallery/gallery/app.dart' show GalleryApp;
import 'package:flutter/material.dart';
void main() {
enableFlutterDriverExtension();
runApp(const GalleryApp(testMode: true));
}
import 'dart:async';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() {
group('scrolling performance test', () {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
if (driver != null)
driver.close();
});
test('measure', () async {
final Timeline timeline = await driver.traceAction(() async {
await driver.tap(find.text('Material'));
final SerializableFinder demoList = find.byValueKey('GalleryDemoList');
// TODO(eseidel): These are very artificial scrolls, we should use better
// https://github.com/flutter/flutter/issues/3316
// Scroll down
for (int i = 0; i < 5; i++) {
await driver.scroll(demoList, 0.0, -300.0, const Duration(milliseconds: 300));
await Future<void>.delayed(const Duration(milliseconds: 500));
}
// Scroll up
for (int i = 0; i < 5; i++) {
await driver.scroll(demoList, 0.0, 300.0, const Duration(milliseconds: 300));
await Future<void>.delayed(const Duration(milliseconds: 500));
}
});
TimelineSummary.summarize(timeline)
..writeSummaryToFile('home_scroll_perf', pretty: true)
..writeTimelineToFile('home_scroll_perf', pretty: true);
});
});
}
~/A/flutter_apptest> flutter drive --target=test_driver/scroll_perf.dart
Using device Android SDK built for x86.
Starting application: test_driver/scroll_perf.dart
Initializing gradle... 1.0s
Resolving dependencies... 3.7s
Installing build/app/outputs/apk/app.apk... 6.3s
Gradle task 'assembleDebug'...
Gradle task 'assembleDebug'... Done 15.8s
Built build/app/outputs/apk/debug/app-debug.apk.
Installing build/app/outputs/apk/app.apk... 6.1s
I/flutter ( 7309): Observatory listening on http://127.0.0.1:33656/
00:00 +0: scrolling performance test (setUpAll)
[info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:55261/
[trace] FlutterDriver: Isolate found with number: 518742094
[trace] FlutterDriver: Isolate is paused at start.
[trace] FlutterDriver: Attempting to resume isolate
[trace] FlutterDriver: Waiting for service extension
[info ] FlutterDriver: Connected to Flutter application.
00:00 +0: scrolling performance test measure
00:14 +1: scrolling performance test (tearDownAll)
00:14 +1: All tests passed!
Stopping application instance.
00:14 +1: All tests passed!
となり、Integration Testが成功していることがわかります!
テスト中のエミュレータは、以下のように動作します!
まとめ
Flutterにおける3種類のテストについて記載しました。
余談ですが、Dartにおけるtestライブラリにもいくつか種類があり、test_api
→ test_core
→ test
という順序で包含関係が成り立っています。(test
がtest_core
を内包し、test_core
がtest_api
を内包しています。)
実は、flutter galleryでは、依存関係を極力少なくするために、test
からtest_api
に依存先を変更するというIssueがあり、すでにmasterに反映されています。
今回は公式ページに則ってtest_api
ではなくtest
を使用したテストを記載しましたが、今後どちらのテストライブラリを使用することが推奨されるかは変わっていくかもしれません。
実際には依存関係があるので、書き方自体はほとんど変わりません。Unit Testに記載しているソースコードの1行目をimport 'package:test/test.dart';
からimport 'package:test_api/test_api.dart';
に変更しても、問題なく動作します。
今後、どちらのライブラリが推奨されるのかも要チェックですね。