flutter_testの導入と書き方をざっくりとまとめてみた

[Testing Flutter App](https://flutter.dev/docs/testing)を写経しました。
この記事では、後述する`Unit Test`についてまとめます。

## Flutterの自動テスト
Flutterでの自動テストは、以下のように分類できます。

* `Unit Test`: 関数やメソッド、クラスのロジックが正しいか検証します。
* `Widget Test`: Widgetが期待通りに動作するか、期待通りのUIを実現できているか検証します。
* `Integration Test`: アプリそのものや機能の大部分をテストします。Widgetやサービスを結合させて動作した時、期待通りに動作するか検証します

`Unit Test`はコストが低く、高速にテストできます。

## Unit Test
ここでは、`flutter_test`を使ったテストの書き方を説明します。
Unit Testの効果的な書き方や考え方は別な記事を参考にしましょう。

### `flutter_test`パッケージを導入する
`flutter_test`を導入するため、以下のようにpubspec.yamlに追記します。

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

### テストを書いてみる
今回は`Counter`というクラスにテストを書きます。
`lib/counter.dart`を作成しましょう。

```dart
class Counter {
  int value = 0;
  void increment() => value++;
  void decrement() => value--;
}
```
`test/counter_test.dart`を追加し、テスト用のコードを書きます。

`test()`でテストケースを定義し、`expect()`で期待通りの結果が得られているか検証します。

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

void main() {
  // group()でテストをグルーピングできる
  group('Counter', () {
    test('Counterの初期値は0', () {
      expect(Counter().value, 0);
    });

    test('increment()を呼び出すと1増える', () {
      final counter = Counter();
      counter.increment();
      expect(counter.value, 1);
    });

    test('decrement()を呼び出すと1減る', () {
      final counter = Counter();
      counter.decrement();
      expect(counter.value, -1);
    });
  });
}
```

このサンプルでは簡単な例ですが、他にも非同期のテストや`Matcher`を使った検証もできます。
[リファレンス](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html)を読みましょう。

### `Mockito`の導入
テスト対象の振る舞いが別なクラスに依存している場合、モックライブラリの`Mockito`を使ってモックしましょう。

今回はhttpで通信した結果をモックしてみます。
例によってpubspec.yamlに追記します。

```yaml
dependencies:
  flutter:
    sdk: flutter
  http:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito:
```
`lib/main.dart`に、こんな感じでHTTPで通信してJSONをパースする処理を書きます。

```dart
import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;

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) {
    return Post.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to load post');
  }
}
```
今回のクラスをテストするには、ネットワーク環境を準備したり通信先のサーバーを準備したりサーバーが狙い通りに動作するように準備したりと、準備がとても大変です。

なので、`Mockito`を使ってサーバーとの通信が成功したように振る舞わせてテストしましょう。

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

// モック対象のクラスを宣言します。
class MockClient extends Mock implements http.Client {}

void main() {
  group('fetchPost', () {
    test('returns a Post if the http call completes successfully', () async {
      // モックを生成します。
      final client = MockClient();
      // client.get()を呼び出した時に、どういうResponseを返すかモックします。
      // このテストでは200を返すケースをモックしてみます。
      when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
          .thenAnswer((_) async => http.Response('{"title" : "Test"}', 200));
      expect(isInstanceOf<Post>(fetchPost(client)), isTrue);
    });

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

      // このテストでは404を返すケースをモックしてみます。
      when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchPost(client), throwsException);
    });
  });
}
```

`Mockito`はモックだけでなく、スタブ・Fake（Partial Mock）の使い方も可能です。
[公式のドキュメント](https://pub.dev/documentation/mockito/latest/)を読んでみましょう。