FlutterでUnitTestしてみた

# 導入
過去に私はモバイルアプリエンジニアとしてFlutterでアプリを作っていました。
当時は、「実装する」ことがメインになっていたので、テストコードを書いていませんでした。
(バックエンド側はユニットテストコードを書いていましたが、モバイル側は意識していませんでした。)

今になって色々調べてみると、Flutterには、
- Unit Test
- Widget Test
- Integration Test

の3種類があるということを知りましたので、
試しにこれらを実装してみることにしました。

まず今回はUnit Testを実装してみたので、その解説をしていきます。

# テスト実施

## テスト対象
以下のproviderをテスト対象としました。

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

final listProvider =
    StateNotifierProvider.autoDispose<ListStateNotifier, ListState>((ref) {
  return ListStateNotifier();
});

@immutable
class ListState {
  const ListState({
    this.dateList = const [],
  });
  final List<String> dateList;

  ListState copyWith({
    required List<String> dateList,
  }) {
    return ListState(
      dateList: dateList,
    );
  }
}

class ListStateNotifier extends StateNotifier<ListState> {
  ListStateNotifier() : super(const ListState());

  void addDate() {
    String date = DateTime.now().toString();
    state = state.copyWith(
      dateList: [...state.dateList, date],
    );
  }
}
```

## テストコード
上記のプロダクションコードに対して、以下のテストコードを書きました。

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:hoge/presentation/view_models/list_provider.dart';

void main() {
    group('ListStateNotifier', () {
        late ListStateNotifier listStateNotifier;

        setUp(() {
            listStateNotifier = ListStateNotifier();
        });

        test('addDate should add a new date to the dateList', () {
            final initialDateList = listStateNotifier.state.dateList;
            final expectedDate = DateTime.now().toString();

            // テスト対象の関数実行
            listStateNotifier.addDate();

            // テスト結果検証
            expect(
                listStateNotifier.state.dateList,
                [...initialDateList, expectedDate]
            );
        });
    });
}
```

`addDate()`メソッドは「ListStateのdateListに現在日時を追加する」関数です。
テストコードでは、このメソッドにより、正しくListStateのdateListに現在日時が書き込まれているかを検証しています。

テストコードとしては以下の実装に問題はありません。
Flutterのユニットテストでは、
`test`メソッドの中で、実行したいテストの内容を書いていきます。
テスト対象のメソッドを実行した後、実際の結果と期待する結果とを`expect`メソッド内で、比較し、一致していれば、テストが成功します。

ただ、今回はテストに失敗してしまいます。

### テスト失敗の理由
失敗した理由は、プロダクションコードの`addDate()`内で、**`DateTime.now()`**を利用しているからです。

テストコードでは、期待する結果用の日付は
`final expectedDate = DateTime.now().toString();`
という実装の中で、生成されます。
一方、テストで`addDate()`を呼び出したタイミングと、expectedDateが定義されたタイミングはミリ秒レベルで一致しない可能性が高く、誤差が生じます。
その結果、テストが失敗してしまいました。

## DateTime.now()を利用する場合のテスト
対策としては、`clock`というライブラリを利用することが挙げられます。
このライブラリを使用すると、テストの際、日時をインジェクションすることができます。

https://pub.dev/packages/clock

### テスト対象修正

```diff_dart:ListProvider.dart
+ import 'package:clock/clock.dart';

// 省略

- String date = DateTime.now().toString();
+ String date = clock.now().toString();
```
上記のようにDateTimeの呼び出しをclockライブラリの実装に変更します。

## テストコード修正
```diff_dart
+ import 'package:clock/clock.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hoge/presentation/view_models/list_provider.dart';

void main() {
    group('ListStateNotifier', () {
        late ListStateNotifier listStateNotifier;

        setUp(() {
            listStateNotifier = ListStateNotifier();
        });

        test('addDate should add a new date to the dateList', () {
            final initialDateList = listStateNotifier.state.dateList;
-            final expectedDate = DateTime.now().toString();
+            final fixedTime = DateTime(2024, 6, 30);
+            final expectedDate = fixedTime.toString();

+            withClock(Clock.fixed(fixedTime), () {
        
                // テスト対象の関数実行
                listStateNotifier.addDate();

                // テスト結果検証
                expect(
                    listStateNotifier.state.dateList,
                    [...initialDateList, expectedDate]
                );
+            });
        });
    });
}
```
ポイントとしては以下の通りとなります。

- テスト用に使用する日付を`fixedTime`という変数で定義しています
- `withClock`メソッドの第一引数でClockの時間をfixedTimeに置き換えています。これによりプロダクションコードの`clock.now()`にfixedTimeの日付がインジェクションされます

以上により、日付の不一致がなくなり、テストは成功します。

# 終わりに
今回はFlutterのユニットテストを実施してみました。
運がよかったのか悪かったのかDateTimeの実装により、日付のインジェクションを行う方法に気づくことができました。

テストはコードの品質や生産性を上げていくのにとても重要な役割を担っているので、これを参考にテストコードを書いてみてもらえると幸いです。