【Flutter】Providerで最低限のDIを行ってテスタブルなコードにリファクタリングする

[前回の記事](https://qiita.com/tokkun5552/items/ede8460bef4892f48e37)の最後に書きましたが、テストを行う際はclass同士の依存関係が密になっていると、思うようにテストを行うことが出来ません。
これは、ビジネスロジックを記載している部分にDBのインスタンスやFirebaseのインスタンスを持たせてしまうと、初期化処理などが行えないためです。(※シミュレータなどを使って回避することもできるようです)
そこで、今回は前回と同じ[こちら](https://github.com/tokku5552/TODOAppSample-Flutter/tree/v1.1)のサンプルアプリをリファクタリングして、テスタブルなコードに書き換えてみました。

### やったこと
- FlutterでのProviderを用いたTodoアプリをリファクタリング
- Providerを使ってDIする
- test用のrepositoryを自作する

最終的なコードは[こちら](https://github.com/tokku5552/TODOAppSample-Flutter/tree/v1.2)です。

# 前回の記事時点での問題点
[前回](https://qiita.com/tokkun5552/items/ede8460bef4892f48e37)の結論でも書きましたがビジネスロジックを持っているmodelがリポジトリに依存していて、さらにリポジトリの中でDBのインスタンスを取得しています。

```dart:todo_item_repository.dart
class TodoItemRepository {
  static String table = 'todo_item';
  static DatabaseProvider instance = DatabaseProvider.instance;
```

このように書いてしまうと、以下のような問題があります。

- DBのイニシャライズがしにくい（多分出来ない）
- 何らかの方法でテスト実行時にDBが作れたとしても、結果として色々な場所を同時にテストすることになり、問題の切り分けがしにくい

これはRealmなどのローカルDBだけでなくFirestoreなどを使用している場合も同様です。
これを解決するために、DI(Dependency Injection)を取り入れてリファクタリングを行いました。

# DIとは？
DIとはDependency Injectionの略で、そのまま日本語にすると**依存性の注入**です。
詳細については下記の参考を参照ください。
参考：
[DI・DIコンテナ、ちゃんと理解出来てる・・？ - Qiita](https://qiita.com/ritukiii/items/de30b2d944109521298f)
[依存性の注入 - Wikipedia](https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5)

端的に説明するならば、**依存関係を取り除き、外部から受け取るようにすること**です。

例えば以下のようなclassがあったとします。

```dart:hoge.dart
class Hoge{
  final _fuga = Fuga();
  _foo;

  void getFoo() {
   _foo = _fuga.getFoo();
}
```
```dart:fuga.dart
class Fuga{
  final _db = HogeHogeDB();

  Foo getFoo() {
   return _db.getFoo();
}
```

`Hoge`クラスの中で`Fuga`のコンストラクタを呼び、インスタンスを作成しています。
これを**HogeクラスはFugaに依存している**といいます。
これが例えば`Fuga`クラスがDBやAPIを呼ぶクラスだった場合、テストが非常にしにくくなってしまいます。
そこでまず外から`Fuga`クラスを渡すようにします。

```dart:hoge.dart
import '../fuga.dart';

class Hoge {
  Hoge ({
    @required Fuga fuga,
  }) : _fuga = fuga;
  Fuga _fuga;
  _foo;

  void getFoo() {
   _foo = _fuga.getFoo();
}
```

`Fuga`を受け取って内部の`_fuga`で参照するようにします。
しかしこれだけでは結局`Fuga`に直接依存してしまうので、`Fuga`の内部でDBを生成することになり単体テストがしやすくなっていません。
そこで`Fuga`クラスをインターフェースと実装に分離します。

```dart:fuga.dart
abstract class Fuga{
  Foo getFoo();
}
```
```dart:fuga_impl.dart
class FugaImpl implements Fuga{
  final _db = HogeHogeDB();

  @override
  Foo getFoo() {
   return _db.getFoo();
}
```
dartには**interface**がないので、**abstract**クラスとして宣言し、インターフェースのように使います。
そして`Hoge`クラスは**abstract**の方のFugaに依存するように書き換えます。(上記の`Hoge`は`Fuga`に依存していたので実際は書き換えなくても**abstract**に依存するようになりました)

ではこの`FugaImpl`はどこで使うのでしょうか？実際のアプリ上ではこちらを使わないとdbからの読み込みが出来ないです。
そこで最後に**Provider**で`Hoge`のコンストラクタに渡してあげます。

```dart:main.dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<Fuga>(
          create: (_) => FugaImpl (),
        ),
      ],
      child: App(),
    ),
  );
}
```

こうすることで`App`以下の`context`から`Fuga`というキーワードで`FugaImpl`を呼べるようになります。
実際に使う際には**ChangeNotifierProvider**や**Cosumer**などで

```dart:
create: (_) => Hoge(
        fuga: context.read<Fuga>(),
      )
```
としてやることで、`Hoge`に`Fuga`を実装した`FugaImpl`を渡すことができます。

# リファクタリング- 依存関係を分離する
では実際に[前回の記事](https://github.com/tokku5552/TODOAppSample-Flutter/tree/v1.1)で作成したTODOアプリのサンプルをリファクタリングしてみます。
サンプルアプリの主な構成は以下のようになっています。
![todosample-flutter_class_diagramm.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141823/e3016263-ee67-f917-a1b7-3e0f5a6c050e.jpeg)

フォルダ構成は以下のようになっています。
![フォルダ構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141823/7aadf0d1-12d5-47e0-4390-9e4fb2e0f374.png)

テストをすべてに対して書いても労力の割には品質に貢献しないということで、[こちら](https://kabochapo.hateblo.jp/entry/2019/11/21/160759)の記事を参考に、単体テストは**ビジネスロジックに関してのみ行うのが効率が良い**ようなので、それを念頭に置いてリファクタリングを行いました。
結果**DDD(Domain Driven Design)とMVVMの間のような形**に落ち着きました。

サンプルアプリはもともとmodelの中でdbを生成するようなことはしていないので、repositoryの分離を行いました。
※modelの中でdbやfirebaseの処理を記述してしまっている場合は、repository_impl側に持たせるようにしてください。

元々書いていたメソッドを抽出してabstractクラスを作ります。

```dart:domain/todo_item_repository.dart
abstract class TodoItemRepository {
  Future<TodoItem> create(String title, String body, bool isDone, DateTime now);
  Future<List<TodoItem>> findAll({bool viewCompletedItems});
  Future<TodoItem> find(int id);
  Future<void> update(TodoItem todoItem);
  Future<void> updateIsDoneById(int id, bool isDone);
  Future<void> delete(int id);
}
```

そして、実際の処理は上記の`TodoItemRepository`を`implements`して、全てのメソッドを`override`します。
**extends**と**implements**の違いは以下を参考にしてください。
[Dart 継承を理解してみる - Qiita](https://qiita.com/takyam/items/212bb961d206c2297453)


```dart:
class TodoItemRepositoryImpl implements TodoItemRepository {
  static String table = 'todo_item';
  static DatabaseProvider instance = DatabaseProvider.instance;

  @override
  Future<TodoItem> create(
      String title, String body, bool isDone, DateTime now) async {
    final Map<String, dynamic> row = {
      'title': title,
      'body': body,
      'createdAt': now.toString(),
      'updatedAt': now.toString(),
      'isDone': (isDone == true) ? 1 : 0,
    };
    final db = await instance.database;
    final id = await db.insert(table, row);
    return TodoItem(
      id: id,
      title: row["title"],
      body: row["body"],
      createdAt: now,
      updatedAt: now,
      isDone: isDone,
    );
  } 
.
.
.
```

そしてmodel側でabstractの方を受け取るようにコンストラクターを書き換え、同時にmainでProviderを使って生成しておきます。

```dart:todo_item_detail_model.dart
class TodoItemDetailModel extends ChangeNotifier {
  TodoItemDetailModel({
    @required TodoItemRepository todoItemRepository,
  }) : _todoItemRepository = todoItemRepository;
  TodoItemRepository _todoItemRepository;
```

```dart:main.dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<StorageRepository>(
          create: (_) => StorageRepositoryImpl(),
        ),
        Provider<TodoItemRepository>(
          create: (_) => TodoItemRepositoryImpl(),
        )
      ],
      child: App(),
    ),
  );
}
```
※SharedPreferenceでローカルに情報を保管するStorageRespositoryについてもDIしています。

これで`TodoItemDetailModel `はDBに一切依存しなくなりました。
注意として、メソッドで渡す引数や受け取る返り値に関しても、パッケージ固有のものは使わないようにしてください。
`dart`のプリミティブ型や`domain`の中で定義したエンティティモデルのみを使います。
つまり`repository`の実装側でそれらに変換してあげます。

# テストコードを書く
テストを書くにはもう1つ工夫が必要です。
DBをモデルから分離したので、テスト用の`repository`を書いてあげます。
以下にコードを記載しますが、ほとんど同じように使えるはずです。

```dart:test/infrastracture/todo_item_repository_mem_impl
class TodoItemRepositoryMemImpl implements TodoItemRepository {
  // { id : TodoItem }
  final _data = <int, TodoItem>{};

  // Repository内部やDB側で自動生成されるものを外部から操作できるようにする
  int _currentId = 0;

  int get currentId => _currentId;

  void incrementId() {
    _currentId++;
  }

  void clear() {
    _data.clear();
    _currentId = 0;
  }

  @override
  Future<TodoItem> create(
      String title, String body, bool isDone, DateTime now) {
    final result = TodoItem(
      id: _currentId,
      title: title,
      body: body,
      isDone: isDone,
      createdAt: now,
      updatedAt: now,
    );
    _data[_currentId] = result;
    return Future.value(result);
  }

  @override
  Future<void> delete(int id) {
    _data.remove(id);
    return null;
  }

  @override
  Future<List<TodoItem>> findAll({bool viewCompletedItems = true}) {
    List<TodoItem> result = [];
    final List<TodoItem> todoItems = List<TodoItem>.unmodifiable(_data.values);
    if (!viewCompletedItems) {
      todoItems.forEach((element) {
        result.add(element);
      });
    } else {
      result = todoItems;
    }
    return Future.value(result);
  }

  @override
  Future<TodoItem> find(int id) {
    return Future.value(_data[id]);
  }

  @override
  Future<void> updateIsDoneById(int id, bool isDone) {
    final todoItem = _data[id];
    todoItem.isDone = isDone;
    _data[id] = todoItem;
    return null;
  }

  @override
  Future<void> update(TodoItem todoItem) {
    TodoItem updateData = _data[todoItem.id];
    updateData.title = todoItem.title;
    updateData.body = todoItem.body;
    updateData.updatedAt = todoItem.updatedAt;
    _data[todoItem.id] = updateData;
    return null;
  }
}
```

`Map`型でデータを保持し、各メソッドを`override`します。
以下のようなことに注意して記載してください。

- `Map`は`List<TodoItem>.unmodifiable(_data.values)`のようにして`List`に変換する
- `Future`で返しているときは、`return Future.value(result)` のようにラップして返却する

今回は`Realm`側で`id`を付与するようにしているので、テストDBも`_currentId`を定義して実際のテストの中で`id`を操作できるようにしておきます。
これで実際にテストが書けるようになりました。

```dart:todo_item_detail_test.dart
void main() {
  final TodoItemRepositoryMemImpl repository = TodoItemRepositoryMemImpl();
  final TodoItemDetailModel model =
      TodoItemDetailModel(todoItemRepository: repository);
  final dummyDate = DateTime.now();

  group('add', () {
    test('正常系', () async {
      // 事前準備
      repository.clear();
      final data = TodoItem(
        id: 0,
        title: 'テストタイトル',
        body: 'テストボディ',
        createdAt: dummyDate,
        updatedAt: dummyDate,
      );
      model.todoTitle = data.title;
      model.todoBody = data.body;

      // メソッド実行
      bool isSuccessful = true;
      try {
        await model.add();
      } catch (e) {
        isSuccessful = false;
      }

      // 結果確認
      final result = await repository.findAll();
      expect(isSuccessful, true);
      expect(result.length, 1);
      final item = result.first;
      expect(item.title, data.title);
      expect(item.body, data.body);
      expect(item.isDone, false);
      expect(item.createdAt, isNotNull);
      expect(item.updatedAt, isNotNull);
      expect(item.createdAt, item.updatedAt);
    });
  });
.
.
.
```

以下のように直接テスト用のrepositoryを生成して、modelに渡しています。

```dart:
final TodoItemRepositoryMemImpl repository = TodoItemRepositoryMemImpl();
  final TodoItemDetailModel model =
      TodoItemDetailModel(todoItemRepository: repository);
```

これでテストの中で直接repositoryを操作することができます。


### 実行＆結果確認

[前回の記事](https://qiita.com/tokkun5552/items/ede8460bef4892f48e37#%E3%83%86%E3%82%B9%E3%83%88%E5%AE%9F%E8%A1%8C)で書いたように実行することもできますが、テストを一つ一つ手動で実行するのは面倒です。
以下のコマンドでtestフォルダ内にあるtestをすべて実行することができます。

```bash:
> flutter test
```
以下のようにその場で結果を確認することができます。
![test実行.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141823/3ad3bfb4-c019-6f22-cf8c-1bf55d59e59f.png)


# まとめ
単体テストを書くために、Providerで最小限の労力でDIを行いました。
**ビジネスロジックに対して割と簡単にテストをすることが出来る**ようになりますし、`domain`と`infrastructure`に分けて外部との処理をしっかり分離してやることでコード全体の見通しも良くなりました。

さらに次のステップとしては以下のようなことができれば良いと思いました。

- GitHubにpushするタイミングで自動的にテストを実施してほしい(CI/CD)
- カバレッジを見える化したい

~~そのうち実施したら記事にしようと思います。~~
**記事にしました**
[【Flutter】GitHubActionsでテストと静的解析を自動化する](https://qiita.com/tokkun5552/items/2eb6793501c152dabf33)


### 2021/4/9 追記
こちらの記事に関する発表を行いました。
以下が発表スライドです。
<iframe src="//www.slideshare.net/slideshow/embed_code/key/sYJVD9J7Ve7IaL" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/ShinnosukeTokuda/fluttergithub-actions" title="Flutterで単体テストを行う方法とGitHub Actionsを使った自動化" target="_blank">Flutterで単体テストを行う方法とGitHub Actionsを使った自動化</a> </strong> from <strong><a href="https://www.slideshare.net/ShinnosukeTokuda" target="_blank">ShinnosukeTokuda</a></strong> </div>