Testing Flutter Apps

# やること
[Testing Flutter apps - Flutter](https://flutter.dev/docs/testing#unit-tests)
上記の公式ドキュメントを読みまとめる。

# [Testing Flutter apps - Flutter](https://flutter.dev/docs/testing#unit-tests)
自動テストには以下の3つのカテゴリがある。

* 単体の関数、メソッド、クラスをテストする[Unit test](https://flutter.dev/docs/testing#unit-tests)
* 単体のWidgetをテストする[Widget test](https://flutter.dev/docs/testing#widget-tests)(他のUIフレームワークではComponent testと呼ばれる)
* アプリ全体やアプリの大部分をテストする[Integration test](https://flutter.dev/docs/testing#integration-tests)

一般的には、多くのUnitテスト、Widgetテストがありカバレッジ率が高く、さらに重要なユースケースをカバーした十分なインテグレーションテストがあるアプリが十分にテストされたアプリと言われている。
このアドバイスは、以下に示すように、様々なテスト間にトレードオフがあるという事実に基づいている。

| | Unit | Widget | Integration |
|:--|:--|:--|:---|
| 信頼性 | △ | ○ | ◎ |
| メンテナンスコスト | ◎ | ○ | △ |
| 依存関係 | ◎ | ○ | △ |
| 実行スピード | ◎ | ◎ | △ |

## Unit tests
* Unitテストの目的は、**様々な状況下でのUnitのロジックが正しいことを検証すること**
* テスト対象のUnitの外部依存関係は、一般的にモックアウトされている
* Unitテストでは**ディスクへのRead／Writeや、画面へのレンダリング、ユーザーアクションの受信などは行わない**。

### [An introduction to unit testing](https://flutter.dev/docs/cookbook/testing/unit/introduction)
* Unitテストを書くには、`test`パッケージを使う
* Widgetをテストするための追加のユーティリティーを使うには`flutter_test`パッケージを使う

以下のステップにしたがって、`test`パッケージを使ったUnitテストを書く。

1. `test`または`flutter_test`dependencyを追加する
2. テストファイルを作成する
3. テスト用のクラスを作る
4. テストを書く
5. 複数のテストを`group`にまとめる
6. テストを実行する

`test`パッケージに関するより詳しい情報は、[test package documentation](https://github.com/dart-lang/test/blob/master/README.md)を参照。

#### 1. Add the test dependency
Flutterに依存していないDartパッケージを使っている場合は、`test`パッケージをインポートできる。

```yaml
dev_dependencies:
  test: <latest_version>
```
#### 2. Create a test file
この例では、2つのファイル(counter.dart, counter_test.dart)を作成する。

`counter.dart`ファイルは、テストしたいクラスを含んでおり、`lib/`フォルダに置く。
`counter_test.dart`はそれらに対するテストを含んでおり、`test/`フォルダに置く。

一般的には、テストファイル(`****_test.dart`)はFlutterプロジェクトのルートの`test/`フォルダに置く。

```:フォルダ構成
counter_app/
  lib/
    counter.dart
  test/
    counter_test.dart
```

#### 3. Create a class to test
次に、テスト対象の「Unit」を作成する。
「Unit」とは、関数やメソッド、クラスの別の言い方である。
ここでは、`lib/counter.dart`に`Counter`クラスを作成する。
`Counter`クラスは、初期値0の`value`をインクリメントしたりデクリメントする役割がある。

```dart:lib/counter.dart
class Counter {
  int value = 0;

  void increment() => value++;

  void decrement() => value--;
}
```

>Note:
簡単のため、このチュートリアルはテスト駆動開発のやり方に従っていない。
慣れている場合は、そのようにやっても良い。

#### 4. Write a test for our class
`counter_test.dart`に最初のUnitテストを書いていく。
テストはトップレベルの`test`関数を使って定義され、`expect`関数を使うことで結果が正しいかをチェックすることができる。
この2つの関数は`test`パッケージに含まれている。

```dart:test/counter_test.dart
// Import the test package and Counter class
import 'package:test/test.dart';
import 'package:counter_app/counter.dart';

void main() {
  test('Counter value should be incremented', () {
    final counter = Counter();

    counter.increment();

    expect(counter.value, 1);
  });
}
```
#### 5. Combine multiple tests in a group
関連するテストが複数ある場合は、`test`パッケージに含まれる`group`関数を使ってそれらをまとめることができる。

```dart:test/counter_test.dart
import 'package:test/test.dart';
import 'package:counter_app/counter.dart';

void main() {
  group('Counter', () {
    test('value should start at 0', () {
      expect(Counter().value, 0);
    });

    test('value should be incremented', () {
      final counter = Counter();

      counter.increment();

      expect(counter.value, 1);
    });

    test('value should be decremented', () {
      final counter = Counter();

      counter.decrement();

      expect(counter.value, -1);
    });
  });
}
```

#### 6. Run the tests
InteliJ, VSCode, Terminalでのやり方が紹介されている。

##### IntelliJ

1. `counter_test.dart`を開く
2. `Run`メニューを選択する
3. `Run 'tests in counter_test.dart'`オプションをクリックする
4. 上記の代わりに、キーボードショートカットでもできる

##### VSCode

1. `counter_test.dart`を開く
2. `Debug`メニューを選択する
3. `Start Debugging`オプションを選択する
4. 上記の代わりに、キーボードショートカットでもできる

##### Terminal
```shell
flutter test test/counter_test.dart
```

### [Mock dependencies using Mockito](https://flutter.dev/docs/cookbook/testing/unit/mocking)

UnitテストがWebサービスやデータベースからデータをフェッチするクラスに依存していることがあり、そのような場合はいくつかの理由で不便である。

* 実際のサービスやデータベースを呼び出すと、テストの実行が遅くなってしまう
* 合格するはずのテストが、Webサービスやデータベースの予期しない結果のせいで失敗することがある。これは"Flaky test"で知られている。
* 実際のサービスやデータベースを使って、考えられるすべての成功や失敗のシナリオをテストすることが難しい。

上記の問題を解決するために、これらの依存関係をモック化する。
モック化することで、実際のWebサービスやデータベースをエミュレートすることができ、状況に応じた特定の結果を返すことができる。

一般的には、クラスの代わりの実装を用意することで、依存関係をモック化することができる。
これらの実装を手動で記述するか、`Mockito`パッケージをショートカットとして利用する。

このレシピでは、以下の手順に従ってMockitoパッケージを使った基本のモック化をデモンストレートする。

1. Package dependenciesを追加する
2. テスト対象の関数を作る
3. モックの`http.Client`を使ってテストファイルを作る
4. 各状況のテストを書く
5. テストを実行する

詳細はこちら → [Mockito package](https://pub.dev/packages/mockito)

#### 1. Add the package dependencies

```yaml
dependencies:
  http: <newest_version>
dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: <newest_version>
```

#### 2. Create a function to test
この例では、[Fetch data from the internet](https://flutter.dev/docs/cookbook/networking/fetch-data)の`fetchPost`関数をテストする。
テストするために、以下の2つの変更を加える:
 
* `fetchPost`関数に`http.Client`を渡す。これにより、状況に応じて正しい`http.Client`を渡すことができる。Flutterやサーバーサイドのプロジェクトには`http.IOClient`、ブラウザアプリには`http.BrowserClient`を、そしてテストにはモックの`http.Client`を渡すことができる。
* モック化が難しい静的な`http.get()`メソッドではなく、渡された`client`を使ってインターネットからデータをフェッチする。

```dart:
class Post {
  dynamic data;
  Post.fromJson(this.data);
}

Future<Post> fetchPost(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/posts/1');

  if (response.statusCode == 200) {
    // If the call to the server was successful, parse the JSON.
    return Post.fromJson(json.decode(response.body));
  } else {
    // If that call was not successful, throw an error.
    throw Exception('Failed to load post');
  }
}
```

#### 3. Create a test file with a mock http.Client
次に、`MockClient`クラスを使ってテストファイルを作成する。
`fetch_post_test.dart`ファイルを`test/`に作成する。

`MockClient`クラスは`http.Client`の実装である。
これにより、`fetchPost`関数に`MockClient`を渡すことができ、各テストで様々なhttpレスポンスを返すことができる。

```dart:test/fetch_post_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;

// Create a MockClient using the Mock class provided by the Mockito package.
// Create new instances of this class in each test.
class MockClient extends Mock implements http.Client {}

main() {
  // Tests go here
}
```
#### 4. Write a test for each condition
`fetchPost`関数は以下のいずれかを行う。

1. http呼び出しが成功した場合、`Post`を返却する
2. 失敗した場合、`Exception`を投げる

そのため、上記2つの状況をテストしたい。
`MockClient`を使って、成功のテストには`OK`レスポンスを返し、失敗のテストにはエラーレスポンスを返す。
Mockitoによって提供されている`when()`を使ってこれらの条件をテストする。

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;

// Create a MockClient using the Mock class provided by the Mockito package.
// Create new instances of this class in each test.
class MockClient extends Mock implements http.Client {}

main() {
  group('fetchPost', () {
    test('returns a Post if the http call completes successfully', () async {
      final client = MockClient();

      // Use Mockito to return a successful response when it calls the
      // provided http.Client.
      when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
          .thenAnswer((_) async => http.Response('{"title": "Test"}', 200));

      expect(await fetchPost(client), isA<Post>());
    });

    test('throws an exception if the http call completes with an error', () {
      final client = MockClient();

      // Use Mockito to return an unsuccessful response when it calls the
      // provided http.Client.
      when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchPost(client), throwsException);
    });
  });
}
```
#### 5. テストを実行する
```shell
dart test/fetch_post_test.dart
```

## Widget tests
* Widgetテストの目的は、Widgetが期待通りに表示され動くかを検証すること
* Widgetのテストには複数のクラスが含まれ、適切なライフサイクルコンテキストを提供するテスト環境が必要

例えば、テスト対象のWidgetはユーザーのアクションとイベントを受け取って応答し、レイアウトを表示し、子Widgetをインスタンス化する。
そのため、WidgetテストはUnitテストよりも複雑である。
しかし、Unitテストと同様に、Widgetテストの環境は本格的なUIシステムよりも遥かに単純な実装に置き換えられる。

### [An introduction to widget testing](https://flutter.dev/docs/cookbook/testing/widget/introduction)

Widgetクラスをテストするためには、`test`パッケージの他に、`flutter_test`パッケージで提供される追加のツールがいくつか必要である。

* `WidgetTester`を使用すると、テスト環境でWidgetを作成し、操作できる
* `testWidget()`関数は各テストケースに対して新しい`WidgetTester`を自動で作成し、`test()`関数のかわりに使わる
* `Finder`クラスを使うと、テスト環境のWidgetを検索することができる。
* Widget固有の`Matcher`定数は、テスト環境で`Finder`が1つのWidgetあるいは複数のWidgetを見つけたかを検証するのに役立つ

このレシピでは、上記の機能を以下のステップにそって説明する。

1. `flutter_test`dependenciesを追加する
2. テスト対象のWidgetを作る
3. `testWidgets`を作る
4. `WidgetTester`を使ってWidgetを構築する
5. `Finder`を使ってWidgetを探す
6. `Matcher`を使ってWidgetを検証する

#### 1. Add the flutter_test dependencies

```yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
```

#### 2. Create a widget to test
`title`と`message`を表示するWidgetを作る。

```dart
class MyWidget extends StatelessWidget {
  final String title;
  final String message;

  const MyWidget({
    Key key,
    @required this.title,
    @required this.message,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}
```

#### 3. Create a testWidgets test
`testWidget`関数を使ってWidgetテストを定義し、`WidgetTester`を作成する。

```dart
void main() {
  // Define a test. The TestWidgets function also provides a WidgetTester
  // to work with. The WidgetTester allows you to build and interact
  // with widgets in the test environment.
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    // Test code goes here.
  });
}
```

#### 4. Build the widget using the WidgetTester
`WidgetTester`によって提供される`pumpWidget()`メソッドを使って、テスト環境の中に`MyWidget`をビルドする。
`pumpWidget`は渡されたWidgetをビルドしてレンダリングする。

```dart
void main() {
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    // Create the widget by telling the tester to build it.
    await tester.pumpWidget(MyWidget(title: 'T', message: 'M'));
  });
}
```

##### Note about pump() methods
`pumpWidget()`を最初に実行したあと、`WidgetTester`は同じWidgetをリビルドする追加の方法を提供する。
これは、`StatefulWidget`やアニメーションを使っている時に役に立つ。

例えば、テスト環境では、ボタンを押して`setState()`を呼んでも、Flutterは自動でWidgetをリビルドしてくれない。
以下のメソッドを使うと、FlutterにWidgetをリビルドするように指示できる。

`tester.pump(Duration duration)`:
フレームをスケジュールし、Widgetのリビルドをトリガーする。
`Duration`が指定されると、その分だけ時計を進め、フレームをスケジュールする。
`Duration`が単一のフレームより長い場合でも、複数のフレームをスケジュールしない。
>Note:
アニメーションを開始するには、tickerを開始するために`pump()`を1回(期間指定せずに)呼び出す必要がある。これがないと、アニメーションは実行されない。

`tester.pumpAndSettle()`:
スケジュールされたフレームがなくなるまで、指定された期間で`pump()`を繰り返し呼び出す。
これは基本的にすべてのアニメーションが完了するのを待つ。

#### 5. Search for out widget using Finder
テスト環境のWidgetで、`Finder`を使うことでWidgetツリーから`title`と`message`のText Widgetを探す。
これにより、Widgetが正しく表示されているかを確認できる。

`Finder`を作るには、`flutter_test`パッケージのトップレベルの`find()`メソッドを使う。
ここではText widgetをさがしているので、`find.text()`メソッドを使用する。
Finderに関する詳細はこちら → [Finding widgets in a widget test](https://flutter.dev/docs/cookbook/testing/widget/finders)

```dart
void main() {
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    await tester.pumpWidget(MyWidget(title: 'T', message: 'M'));

    // Create the Finders.
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');
  });
}
```

#### 6. Verify the widget using a Matcher
最後に、`flutter_test`で提供される`Matcher`定数を使って画面に表示されるtitleとmessageのTextWidgetを検証する。
`Matcher`クラスは`test`パッケージのコアパートであり、与えられた値が期待したものと一致するかを検証する共通の方法を提供している。

Widgetが画面に1度だけ表示されることを確認するには、`findsOneWidget`Matcherを使用する。

```dart
void main() {
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    await tester.pumpWidget(MyWidget(title: 'T', message: 'M'));
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // Use the `findsOneWidget` matcher provided by flutter_test to verify
    // that the Text widgets appear exactly once in the widget tree.
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}
```
##### Additional Matchers
* `findsNothing`:
    * Widgetが見つからないことを確認する
* `findsWidgets`:
    * 1つかそれ以上のWidgetが見つかることを確認する
* `findsNWidgets`:
    * 特定の数のWidgetが見つかることを確認する

#### Complete example
```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  // Define a test. The TestWidgets function also provides a WidgetTester
  // to work with. The WidgetTester allows building and interacting
  // with widgets in the test environment.
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    // Create the widget by telling the tester to build it.
    await tester.pumpWidget(MyWidget(title: 'T', message: 'M'));

    // Create the Finders.
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // Use the `findsOneWidget` matcher provided by flutter_test to
    // verify that the Text widgets appear exactly once in the widget tree.
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

class MyWidget extends StatelessWidget {
  final String title;
  final String message;

  const MyWidget({
    Key key,
    @required this.title,
    @required this.message,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}
```

### [Tap, drag, and enter text](https://flutter.dev/docs/cookbook/testing/widget/tap-drag)
多くのWidgetは情報を表示するだけでなく、ユーザーの操作に応答する。例えばタップできるボタンや、文字列を入力できる`TextField`がある。

`WidgetTester`を使えば、テキストの入力や、タップ、ドラッグをシュミレートすることができる。

* `enterText()`
* `tap()`
* `drag()`

大体の場合は、ユーザー操作があったらアプリの状態を更新するが、テスト環境では状態変化が起こった時に自動でリビルドはしてくれないので、`pump()`や`pumpAndSettle()`メソッドを使う必要がある。

1. テストするTextを作る
2. TextFieldに文字列を入力する
3. ボタンをタップしたらTodoが追加されるのを確認する
4. スワイプしたらTodoが削除されるのを確認する

#### 1. Create a widget to test
この例では、以下のような機能を持った簡単なTodoアプリを作る

1. `TextField`にテキストを入力できる
2. `FloatingActionButton`をクリックすると、Todoのリストにテキストを追加できる
3. スワイプすると、リストからアイテムが消える

```dart
class TodoList extends StatefulWidget {
  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  static const _appTitle = 'Todo List';
  final todos = <String>[];
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: Text(_appTitle),
        ),
        body: Column(
          children: [
            TextField(
              controller: controller,
            ),
            Expanded(
              child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (BuildContext context, int index) {
                  final todo = todos[index];

                  return Dismissible(
                    key: Key('$todo$index'),
                    onDismissed: (direction) => todos.removeAt(index),
                    child: ListTile(title: Text(todo)),
                    background: Container(color: Colors.red),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}
```

ここではテストにフォーカスするので、Todoアプリについては触れないが、以下で詳細に学ぶことができる。

* [Create and style a text field](https://flutter.dev/docs/cookbook/forms/text-input)
* [Handle taps](https://flutter.dev/docs/cookbook/gestures/handling-taps)
* [Create a basic list](https://flutter.dev/docs/cookbook/lists/basic-list)
* [Implement swipe to dismiss](https://flutter.dev/docs/cookbook/gestures/dismissible)

#### 2. Enter text in the text field
1. テスト環境にWidgetを用意する
2. `WidgetTester`の`enterText()`メソッドを使う

```dart
testWidgets('Add and remove a todo', (WidgetTester tester) async {
  // Build the widget
  await tester.pumpWidget(TodoList());

  // Enter 'hi' into the TextField.
  await tester.enterText(find.byType(TextField), 'hi');
});
```

#### 3. Ensure tapping a button adds the todo
1. `tap()`メソッドを使って追加ボタンをタップする
2. 状態が変わったら、`pump()`メソッドでWidgetをリビルドする
3. 画面にリストアイテムが現れるかを確認する

```dart
testWidgets('Add and remove a todo', (WidgetTester tester) async {
  // Enter text code...

  // Tap the add button.
  await tester.tap(find.byType(FloatingActionButton));

  // Rebuild the widget after the state has changed.
  await tester.pump();

  // Expect to find the item on screen.
  expect(find.text('hi'), findsOneWidget);
});
```

#### 4. Ensure swipe-to-dismiss remove the todo
1. `drag()`メソッドを使って、スワイプ操作を実行する
2. `pumpAndSettle()`を使って、dismissアニメーションが完了するまで連続的にWidgetツリーをリビルドし続ける
3. 画面にそのアイテムがないことを確認する

```dart
testWidgets('Add and remove a todo', (WidgetTester tester) async {
  // Enter text and add the item...

  // Swipe the item to dismiss it.
  await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));

  // Build the widget until the dismiss animation ends.
  await tester.pumpAndSettle();

  // Ensure that the item is no longer on screen.
  expect(find.text('hi'), findsNothing);
});
```

## Integration tests
* Integration testの目的は、テスト対象のすべてのWidgetとサービスが期待通りに連携することを確認することである。
* アプリのパフォーマンスを確認することもできる
* 一般的には、Integration tesｔは、実機や、iOSシミュレーター・Android EmulatorのようなOSエミュレーター上で実行する。
* 通常、テスト対象のアプリは、結果に影響しないようにするためにテストドライバーコードと分離されている

### [An introduction to integration testing](https://flutter.dev/docs/cookbook/testing/integration/introduction)

Integration testは以下のような組みで実行する
1. アプリを実機やエミュレーターにデプロイする
2. 分離したテストスートでアプリを実行し、正しく動くかを確認する

この組みを作成するためには、`flutter_driver`パッケージを利用する。
このレシピでは、カウンターアプリを例としてテストを行う。
セットアップ方法、アプリに表示された特定の文字の検証方法、Widgetをタップする方法、インテグレーションテストの実行方法を紹介する

1. テスト用のアプリを作る
2. `flutter_driver`dependencyを追加する
3. テストファイルを作成する
4. アプリを道具化する
5. インテグレーションテストを書く
6. テストを実行する

#### 1. Create an app to test
この例では、`flutter create`でできるカウンターアプリをテストする。

テストのため、`Text`と`FloatingActionButton`widgetに`ValueKey`を付与する。
これにより、テストスートの中で識別することができる。

```dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter App',
      home: MyHomePage(title: 'Counter App Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @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',
              // Provide a Key to this specific Text widget. This allows
              // identifing the widget from inside the test suite,
              // and reading the text.
              key: Key('counter'),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // Provide a Key to this button. This allows finding this
        // specific button inside the test suite, and tapping it.
        key: Key('increment'),
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
```

#### 2. Add the flutter_driver dependency

```yaml:pubspec.yaml
dev_dependencies:
  flutter_driver:
    sdk: flutter
  test: any
```

#### 3. Create the test files
UnitテストやWidgetテストとは異なり、Integrationテストスートはアプリがテストされているのと同じプロセスでは動かない。
そのため、`test_driver/`フォルダに以下のようなファイルを2つ作成する。
1. 1つ目のファイルは、アプリの"道具"版を含む。道具化することで、テストスートからアプリを動かし、パフォーマンスを記録できる。このファイルには意味のある任意の名前をつけても良い。例えば、ここでは`test_driver/app.dart`を作成する。
2. 2つ目のファイルは、アプリを動かし、期待通りに動いているかを検証するテストスートを含む。テストスートはパフォーマンスプロファイルも記録する。テストファイルの名前は道具化したアプリを含むファイル名に応じて、末尾に`_test`を追加する必要がある。ここでは、`test_driver/app_test.dart`を作成する。

```:フォルダ構成
counter_app/
  lib/
    main.dart
  test_driver/
    app.dart
    app_test.dart
```

#### 4. Instrument the app
1. flutter driver extensionを有効にする
2. アプリを起動する

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

void main() {
  // This line enables the extension.
  enableFlutterDriverExtension();

  // Call the `main()` function of the app, or call `runApp` with
  // any widget you are interested in testing.
  app.main();
}
```

#### 5. Write the tests
1. `SerializableFinders`を作り、特定のWidgetを見つける
2. テストが動く前に`setUpAll()`関数でアプリに接続する
3. 重要なシナリオをテストする
4. テストが終わったら、`teardownAll()`関数でアプリと接続を解除する

```dart
// Imports the Flutter Driver API.
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {
    // First, define the Finders and use them to locate widgets from the
    // test suite. Note: the Strings provided to the `byValueKey` method must
    // be the same as the Strings we used for the Keys in step 1.
    final counterTextFinder = find.byValueKey('counter');
    final buttonFinder = find.byValueKey('increment');

    FlutterDriver driver;

    // Connect to the Flutter driver before running any tests.
    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    // Close the connection to the driver after the tests have completed.
    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('starts at 0', () async {
      // Use the `driver.getText` method to verify the counter starts at 0.
      expect(await driver.getText(counterTextFinder), "0");
    });

    test('increments the counter', () async {
      // First, tap the button.
      await driver.tap(buttonFinder);

      // Then, verify the counter text is incremented by 1.
      expect(await driver.getText(counterTextFinder), "1");
    });
  });
}
```

#### 6. Run the tests
##### 6a. Mobile
iOSシミュレーターやAndroidエミュレーター、実機などの場合。

```shell
flutter drive --target=test_driver/app.dart
```
上記のコマンドは以下を実行する。

* `--target`で指定されたアプリをビルドし、エミュレーターや実機にインストールする
* アプリを起動する
* `app_test.dart`のテストスートを実行する。

##### 6b. Web
ブラウザに応じたドライバをダウンロードする。
ドライバを起動したら、以下のコマンドでテストを実行する。
※ SafariはMacでのみテスト可能

```shell
flutter drive --target=test_driver/app.dart --browser-name=[browser name] --release
```

### [Handle scrolling](https://flutter.dev/docs/cookbook/testing/integration/scrolling)

Integrationテストでリストのスクロールをするためには、`flutter_driver`パッケージに含まれている`FlutterDriver`クラスのメソッドを使う。

このレシピでは、アイテムのリストをスクロールして特定のWidgetが表示されているか確認する方法を紹介し、様々なアプローチのメリット・デメリットを紹介する。

1. アイテムのリストを作成する
2. アプリを道具化する
3. リストをスクロールするテストを書く
4. テストを実行する

#### 1. Create an app with a list of items

```dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp(
    items: List<String>.generate(10000, (i) => "Item $i"),
  ));
}

class MyApp extends StatelessWidget {
  final List<String> items;

  MyApp({Key key, @required this.items}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final title = 'Long List';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: ListView.builder(
          // Add a key to the ListView. This makes it possible to
          // find the list and scroll through it in the tests.
          key: Key('long_list'),
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(
                '${items[index]}',
                // Add a key to the Text widget for each item. This makes
                // it possible to look for a particular item in the list
                // and verify that the text is correct
                key: Key('item_${index}_text'),
              ),
            );
          },
        ),
      ),
    );
  }
}
```


#### 2. Instrument the app

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

void main() {
  // This line enables the extension.
  enableFlutterDriverExtension();

  // Call the `main()` function of the app or call `runApp` with
  // any widget you are interested in testing.
  app.main();
}
```

#### 3. Write a test that scrolls through the list
この例では、アイテムのリストをスクロールして、リストの中に特定のアイテムがあるかを検証する。
`FlutterDriver`クラスはリストをスクロールするための3つのメソッドを用意している:

* `scroll()`メソッドは指定された分だけ特定のリストをスクロールする
* `scrollIntoView()`メソッドはすでに描画されている特定のWidgetを見つけ、スクロールしてそれを完全に表示する。`ListView.builder`などの一部のWidgetはアイテムをオンデマンドで描画する。
* `scrollUntilVisible()`メソッドは特定のWidgetが見えるまでリストをスクロールする。

以下の理由で、`scrollUntilVisible`が最もロバスト（変化に強い)選択肢である:

1. `scroll()`メソッドを単独で使う場合、リストの各項目の高さを誤って想定している可能性がある。これによって、スクロールが足りなかったり、スクロールしすぎてしまうことがある。
2. `scrollIntoView()`メソッドを使う場合、Widgetがインスタンス化されていて表示されていると想定するかもしれない。幅広いデバイス上でアプリが正常に動くかを検証するには、様々な画面サイズのデバイスに対してIntegrationテストを実行する必要がある。**`ListView.builder`はオンデマンドでアイテムを描画するので、特定のWidgetが描画されたかどうかは画面の大きさに依存してしまう。**

上記の理由により、`scrollUntilVisible()`メソッドを使って、探している物が見つかるまでアイテムのリストを繰り返しスクロールするのがおすすめ。

以下の例では、リストの中から特定のアイテムを見つけて`scrollUntilVisible()`の使い方を示している。

```dart:test_driver/app_test.dart
// Imports the Flutter Driver API.
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Scrollable App', () {
    FlutterDriver driver;

    // Connect to the Flutter driver before running any tests.
    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    // Close the connection to the driver after the tests have completed.
    tearDownAll(() async {
      if (driver != null) {
        await driver.close();
      }
    });

    test('verifies the list contains a specific item', () async {
      // Create two SerializableFinders and use these to locate specific
      // widgets displayed by the app. The names provided to the byValueKey
      // method correspond to the Keys provided to the widgets in step 1.
      final listFinder = find.byValueKey('long_list');
      final itemFinder = find.byValueKey('item_50_text');

      await driver.scrollUntilVisible(
        // Scroll through the list
        listFinder,
        // Until finding this item
        itemFinder,
        // To scroll down the list, provide a negative value to dyScroll.
        // Ensure that this value is a small enough increment to
        // scroll the item into view without potentially scrolling past it.
        //
        // To scroll through horizontal lists, provide a dxScroll
        // property instead.
        dyScroll: -300.0,
      );

      // Verify that the item contains the correct text.
      expect(
        await driver.getText(itemFinder),
        'Item 50',
      );
    });
  });
}
```

#### 4. Run the test

```dart
flutter drive --target=test_driver/app.dart
```

### [Performance profiling](https://flutter.dev/docs/cookbook/testing/integration/profiling)
性能ってUXにとって大事だよね。。。
スムーズなスクロール、フレーム飛びがないアニメーションをユーザーは期待している。
このレシピでは、特定のタスクを実行中の性能タイムラインを保存するテストの書き方と、結果をローカルファイルに保存する方法について説明する: 

1. アイテムリストをスクロールするテストを書く
2. アプリの性能を記録する
3. 結果をディスクに保存する
4. テストを実行する
5. 結果をレビューする

#### 1. Write a test that scrolls through a list of items 
"Handle Scrolling"で使ったコードと同じコードを使用する。

```dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp(
    items: List<String>.generate(10000, (i) => "Item $i"),
  ));
}

class MyApp extends StatelessWidget {
  final List<String> items;

  MyApp({Key key, @required this.items}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final title = 'Long List';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: ListView.builder(
          // Add a key to the ListView. This makes it possible to
          // find the list and scroll through it in the tests.
          key: Key('long_list'),
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(
                '${items[index]}',
                // Add a key to the Text widget for each item. This makes
                // it possible to look for a particular item in the list
                // and verify that the text is correct
                key: Key('item_${index}_text'),
              ),
            );
          },
        ),
      ),
    );
  }
}
```

#### 2. Record the performance of the app
`FlutterDriver`クラスで用意されている`traceAction()`メソッドを使う。

このメソッドは、渡された関数を実行し、アプリの性能に関する詳細情報を含む`Timeline`を記録する。
この例では、アイテムのリストをスクロールする関数を渡し、指定のアイテムが表示されているかを確認する。
渡した関数が完了すると、`traceAction()`メソッドは`Timeline`を返す。

```dart
// Record a performance timeline as the app scrolls through the list of items.
final timeline = await driver.traceAction(() async {
  await driver.scrollUntilVisible(
    listFinder,
    itemFinder,
    dyScroll: -300.0,
  );

  expect(await driver.getText(itemFinder), 'Item 50');
});
```

#### 3. Save the results to disk
`Timeline`オブジェクトは起こったイベント全てに関する詳細な情報を持っているが、結果をレビューする良いやり方は提供していない。
そのため、`Timeline`を`TimelineSummary`に変換する。
`TimelineSummary`は結果をレビューしやすくするために2つのタスクを実行する。

1. `Timeline`に含まれているデータをまとめて、ディスクにjson形式で出力する。
2. 完全な`Timeline`をjsonでディスクに保存する。このファイルによって、Chromeのトレーシングツール(`chrome://tracing`)で開けるようになる。

```dart
// Convert the Timeline into a TimelineSummary that's easier to read and
// understand.
final summary = new TimelineSummary.summarize(timeline);

// Then, save the summary to disk.
summary.writeSummaryToFile('scrolling_summary', pretty: true);

// Optionally, write the entire timeline to disk in a json format. This
// file can be opened in the Chrome browser's tracing tools found by
// navigating to chrome://tracing.
summary.writeTimelineToFile('scrolling_timeline', pretty: true);
```

#### 4. Run the test
`--profile`オプションをつけると、"デバッグモード"ではなく"プロファイルモード"でアプリをコンパイルでき、エンドユーザーにより近い性能を検証できる。

```shell
flutter drive --target=test_driver/app.dart --profile
```

#### 5. Review the results
テストが正常に終了すると、`build`フォルダに以下の2つのファイルがある

1. `scrolling_summary.timeline_summary.json`は要約を含むファイルである。テキストエディタなどでひらけば内容を確認できる。よりしっかりレビューするためには、毎回このファイルを作成して、結果のグラフなどを作るとよい。
2. `scrolling_timeline.timeline.json`は完全な`Timeline`のデータである。Chromeのトレーシングツール(`chrome://tracing`)で開くことができ、見やすくなるので、性能に関する問題を発見することができる。

#### Complete example

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

void main() {
  group('Scrollable App', () {
    FlutterDriver driver;

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

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

    test('verifies the list contains a specific item', () async {
      final listFinder = find.byValueKey('long_list');
      final itemFinder = find.byValueKey('item_50_text');

      // Record a performance profile as the app scrolls through
      // the list of items.
      final timeline = await driver.traceAction(() async {
        await driver.scrollUntilVisible(
          listFinder,
          itemFinder,
          dyScroll: -300.0,
        );

        expect(await driver.getText(itemFinder), 'Item 50');
      });

      // Convert the Timeline into a TimelineSummary that's easier to
      // read and understand.
      final summary = new TimelineSummary.summarize(timeline);

      // Then, save the summary to disk.
      summary.writeSummaryToFile('scrolling_summary', pretty: true);

      // Optionally, write the entire timeline to disk in a json format.
      // This file can be opened in the Chrome browser's tracing tools
      // found by navigating to chrome://tracing.
      summary.writeTimelineToFile('scrolling_timeline', pretty: true);
    });
  });
}
```