LoginSignup
70
47

More than 5 years have passed since last update.

Flutterの3種類のソースコードテスト(Unit Test, Widget Test, Integration Test)についてのまとめ

Last updated at Posted at 2018-12-12

この記事は全部俺 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では、以下のようにエミュレータ上で実際にスクロールを行うようなテストを記載する事もできます。(わかりにくいですが、人間は何も操作せず、全自動でスクロールが行われています。)
scroll.gif

Unit Test

ビジネスロジックに対するテストを記載します。
BLoCパターンで言うビジネスロジックももちろんこちらに記載しますが、それ以外でも単純な関数などはこちらに記載することになります。
まず、以下のようにtestパッケージを依存関係に含めます。

pubspeck.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  test:

そして、以下のようなファイルを作成してテストを実行すると、実行結果が出力されるはずです。

test/unit_test.dart
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です。

test/unit_test.dart
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を使用してヘッドレスブラウザでのテストを記載したことのある人はイメージしやすいかもしれません。

test/widget_test.dart
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では、以下の順番でテストを実行します。

  1. 依存ライブラリflutter_driverのインストール
  2. FlutterDriverExtension機能の有効化
  3. エミュレータを起動してアプリケーションを起動
  4. テストコードを記載して実行

pubspeck.yamlに以下のように記載します。

pubspeck.yaml
dev_dependencies:
  flutter_driver:
    sdk: flutter

Integration Testのディレクトリ構造は、Unit Testや Widget Testと違い、myapp/test/xxx_test.dartという配置ではなく、myapp/test_driver/xxx.dartmyapp/test_driver/xxx_test.dartという配置にします。
Integration Testでは2ファイル作成する必要があるので要注意です。

以下のソースコードは、flutter_galleryのテストコードをお借りしました。
flutter_galleryでスクロールを行う際のテストコードになります。

test_driver/scroll_perf.dart
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));
}

test_driver/scroll_perf_test.dart
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が成功していることがわかります!
テスト中のエミュレータは、以下のように動作します!

scroll.gif

まとめ

Flutterにおける3種類のテストについて記載しました。
余談ですが、Dartにおけるtestライブラリにもいくつか種類があり、test_apitest_coretestという順序で包含関係が成り立っています。(testtest_coreを内包し、test_coretest_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';に変更しても、問題なく動作します。
今後、どちらのライブラリが推奨されるのかも要チェックですね。

参考文献

Testing Flutter apps

70
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70
47