Flutterのテストについて調べた

#はじめに
Flutterのテストって一体どんな風にやるんだろう？もしくは何ができるんだろうと思ったので、テストについて色々と調べたことを書きたいと思います。

#テストの種類
Flutterにおいて、テストは３種類あります。
ユニットテスト、ウィジェットテスト、インテグレーションテストがあります。各テストは下記のような内訳になります。

||ユニット|ウィジェット|インテグレーション|
|---|:---:|:---:|:---:|
|信頼度|低|高|最高|
|メンテナンスコスト|低|高|最高|
|依存度|少|多|めっちゃ多い|
|実行速度|速い|遅め|めっちゃ遅い|

##ユニットテスト

testパッケージを導入します。

```yaml:pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  test: ^1.5.1
```

簡単なテストを書いてみる

```dart:test/unit_test.dart
import 'package:test/test.dart';

void main() {
  test('my first unit test', (){
    var answer = 2;
    expect(answer, 2);
  });
}
```

テストを走らせるコマンドを入力すると動きます。

```bash:テスト成功
~$ flutter test
00:03 +2: All tests passed! 
```

```bash:テスト失敗
~$ flutter test
00:02 +0 -1: /Users/kawakami/study/flutter/sample/test/unit_test.dart: my first unit test [E]                                                                                                   
  Expected: <5>
    Actual: <2>
  
  package:test_api    expect
  unit_test.dart 6:5  main.<fn>
  
00:03 +1 -1: Some tests failed.  
```

単一ファイルを動かすときは下記のようにします。

```bash
~$ flutter test test/unit_test.dart
```

##ウィジェットテスト
Flutterプロジェクトを立ち上げた時にデフォルトでカウントアップのプログラムのテストコードが書いているので、それをベースに見てみます。デフォルトプログラムの実行結果が下図です。
![test.gif](https://qiita-image-store.s3.amazonaws.com/0/199445/ecf98039-cbf8-e404-3296-6d46ef2aa943.gif)

```dart:test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:sample/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });

}
```

まずユニットテストの違いでいうとflutter用のテストパッケージを利用します。

```dart
import 'package:flutter_test/flutter_test.dart';
```

```dart
testWidgets('Counter increments smoke test', (WidgetTester tester) 
```

testWidgets関数を使い、第二引数にWigetTester引数の関数を定義するようになっています。

```dart
await tester.pumpWidget(MyApp());
```

ここのpumpWidgetでアプリの立ち上げをやります。
そのあとは実際に画面が立ち上がっているイメージで操作をして変化をテストできます。

```dart
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);
```

この部分で起動時の状態のテストを行なっています。findsOneWidgetやfindsNothingは用意されているオブジェクトです。findsOneWidgetは期待されるウィジェットが１つ探すやつで、findsNothingは期待されるウィジェットが一つもないという状態をテストします。

```dart
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
```

ここでは実際にボタンをtapしてカウントさせるアクションをしています。その後変化を起こすためにtester.pumpで状態変更します。

```dart
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
```

最後は１にカウントされた状態のテストが書かれています。



## インテグレーションテスト
エミュレータを使ってテストコードが実行されます！
flutter driverというパッケージを使います。
まずはパッケージの導入です

```yaml:pubspec.yml
dev_dependencies:
  flutter_driver:
    sdk: flutter
```

テストファイルはtest_driverというフォルダを作成してそちらに作成していきます
ファイル名はapp.dartとその名前に_testをつけたapp_test.dartとします。
appの部分はそれぞれ任意で変更して構いません。app.dartはapp_test.dartを動かすようになっていますので、app部分の名前は一致させておく必要があります。

```bash
lib
res
test
test_driver
|____app.dart
|____app_test.dart
```

インテグレーションテストですべきことは
①flutter driver機能をONにする
②メインアプリを起動する
③テストコードを書く
まず①と②を動作をするコードをtest_driver/app.dartに書いていきます。

```dart:test_driver/app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:sample/main.dart' as app;

void main() {
  enableFlutterDriverExtension();
  app.main();
}
```

次に③の実際のテストコードを書きます。
テストの内容は「カウントアップするアプリのボタンを押したら０から１になる」というテストです。
ただテストを書く前に、表示されるテキストのウィジェットや加算するためのボタンなどの操作をどうすればいいのでしょうか？どうやって指定したらいいのでしょうか？
それを解決するためにはアプリ側のウィジェットにkeyプロパティがあるのでそれで特定のウィジェットを指定できるようにします。

下記のコードのTextウィジェットとFloatingActionButtonウィジェットにkeyプロパティを設定しています。Keyクラスの引数に名前を入れるとその名前でアクセスできるようになります（わかりやすい！）

```dart:lib/main.dart
/// 省略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
              key: Key('counter'),　// 追加
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: Key('increment'), // 追加
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
```

ではテストコードを書いていきます。

```dart:test_driver/app_test.dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {

    final counterTextFinder = find.byValueKey('counter');
    final buttonFinder = find.byValueKey('increment');

    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('starts at 0', () async {
      expect(await driver.getText(counterTextFinder), "0");
    });

    test('increments the counter', () async {
      await driver.tap(buttonFinder);
      expect(await driver.getText(counterTextFinder), "1");
    });
  });
}

```

setUpAll関数はテスト実行前に動作するもので主に準備用に使っています。ここではFlutterDriverにコネクトする処理が書いてあります。
tearDownAllはテスト終了時に実行されるものです。setUpAllで接続した状態をクローズする処理が書いてあります。

あとはtest関数を用いてテストを書いていくだけです。
driver.getTextで指定したウィジェットのテキストが取れます。今回加算されているウィジェットを指定しています。

```bash:実行
~$ flutter drive --target=test_driver/app.dart
Using device iPhone XR.
Starting application: test_driver/app.dart
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    2.2s                                                                                                                                       
 └─Compiling, linking and signing...                  2.7s                                                                                                                                       
Xcode build done.                                            6.9s                                                                                                                            4.4s
flutter: Observatory listening on http://127.0.0.1:55309/
00:00 +0: Counter App (setUpAll)
[info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:55309/
[trace] FlutterDriver: Isolate found with number: 985713800
[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:01 +0: Counter App starts at 0
00:01 +1: Counter App increments the counter
00:01 +2: Counter App (tearDownAll)
00:01 +2: All tests passed!
Stopping application instance.

```

実際に実行すると下図のようにエミュレータ上でテストコードが実行されます！すごく地味ですがテストコードによりボタンが押されて加算されている状態がわかります。

![drive1.gif](https://qiita-image-store.s3.amazonaws.com/0/199445/3bc364f3-faea-22d6-b88c-a23a4a436ad7.gif)



#おわりに
Flutterのテストに触れてみましたが導入もとても楽でわかりやすくてとても書いていて楽しかったです。