FlutterでWidgetTestしてみた

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

今になって色々調べてみると、Flutterには、

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

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

# テスト実施
## テスト対象
以下のWidgetをテスト対象としました。
ちなみにソース上の`listProvider`は前回のUnit Testの記事と同じものになります。

https://qiita.com/phibi-soon/items/04a7018e3a1205ad0137#%E3%83%86%E3%82%B9%E3%83%88%E5%AF%BE%E8%B1%A1

もし、ソースが気になる方はそちらを参照してください。

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hoge/presentation/view_models/list_provider.dart';

class ExpandedListView extends ConsumerWidget {
  const ExpandedListView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final listState = ref.watch(listProvider);
    return Expanded(
      child: ListView.builder(
        shrinkWrap: true,
        physics: const NeverScrollableScrollPhysics(),
        scrollDirection: Axis.vertical,
        itemCount: listState.dateList.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            margin: const EdgeInsets.all(10),
            padding: const EdgeInsets.all(10),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.primary,
              borderRadius: BorderRadius.circular(10),
            ),
            child: Text(
              listState.dateList[index],
              style: Theme.of(context).textTheme.bodyText1,
            ),
          );
        },
      ),
    );
  }
}

```

`listProvider`には、`dateList`の中にString型で日付が格納されています。
それを今回のWidgetでは、`dateList`の中身をContainerとして出力し、Textで日付を表示するものになっています。

今回のWidgetテストでは、
- `dataList`のデータ数がContainer数であること
- `dataList`の中身とTextで表示されている文字(日付)が同じであること
を満たすことを観点としました。

## テストコード

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hoge/presentation/view_models/list_provider.dart';
import 'package:hoge/presentation/widgets/expanded_list_view.dart';

void main() {
  const mockState = ListState(dateList: ['2023-01-01', '2023-01-02']);

  // Container Widgetの数の検査
  testWidgets('ExpandedListView displays correct number of items',
      (WidgetTester tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          listProvider.overrideWith((ref) => MockListStateNotifier(mockState)),
        ],
        child: const MaterialApp(
          home: Scaffold(
            body: Column(
              children: [
                ExpandedListView(),
              ],
            ),
          ),
        ),
      ),
    );

    // ListViewが1つであること
    expect(find.byType(ListView), findsOneWidget);
    // ContainerがmockState.dataListと同数であること
    expect(find.byType(Container),findsNWidgets(mockState.dateList.length));
    
  });

  testWidgets('ExpandedListView displays correct text',
      (WidgetTester tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          listProvider.overrideWith((ref) => MockListStateNotifier(mockState)),
        ],
        child: const MaterialApp(
          home: Scaffold(
            body: Column(
              children: [
                ExpandedListView(),
              ],
            ),
          ),
        ),
      ),
    );

    // Textに表示されている文言がmockState.dateListの中身と一致していること
    expect(find.text(mockState.dateList[0]), findsOneWidget);
    expect(find.text(mockState.dateList[1]), findsOneWidget);
  });
}

// 重要: ListStateNotifierのMock用のクラス
class MockListStateNotifier extends ListStateNotifier {
  MockListStateNotifier(ListState initialState) : super() {
    state = initialState;
  }
}

```

テストは問題なく成功しました。
ただ、1点重要な点が含まれます。
それは、今回のテスト対象にはProviderが含まれていることです。
ProviderのStateは通常デフォルト値が設定されており、今回の`ListStateNotifier`においても「空のdateList」がデフォルトとなっていました。
したがって、テストコードに書いてある`mockState`をテスト対象のProviderのdateListに格納することができません。

そこで、テストコードの最後に定義している`MockListStateNotifier`というクラスが役立ちます。
これは`ListStateNotifier`を継承しているため、`ListStateNotifier`として振る舞うことが可能です。
このクラスを`tester.pumpWidget`の中で、利用することで、dataListの`mockState`のリストを設定することができています。

- 利用箇所

```dart
ProviderScope(
  overrides: [
      listProvider.overrideWith((ref) => MockListStateNotifier(mockState)),
  ],
  // 省略
)
```

# 最後に

今回はWidget Testを書いてみました。
このテストはWebアプリにおけるフロントエンド開発のComponentテストに近いものになります。
ただ、Widget Testという言葉でいうとFlutterの特徴の一つであるWidgetという単位でのテストになり、Flutter固有のテストとも言えます。
これが誰かの役に立てば幸いです。