riverpodとflutter_hooksで作った無限スクロール画面のテストを書く

前回の記事の続きです

https://qiita.com/derakudo/items/f991e6386e5ee764c0f7

ページングの画面のwidgetテストをmockitoを使って書いてみます

# 事前準備
テスタブルなコードに改善します。前回の実装ではViewModelでリポジトリのインスタンスを生成していました。mock化できるようにリポジトリをprovider経由で取得するように変更します

```dart:user_repository.dart
final userRepositoryProvider = Provider((ref) => UserRepository());

class UserRepository {

}
```

ViewModel内では、provider経由でリポジトリのインスタンスを取得します。

```dart:users_view_model.dart


final usersViewModelProvider =
    StateNotifierProvider<UsersViewModel, UsersViewState>(
  (ref) {
    return UsersViewModel(
      // ここでrefを渡す
      ref, 
      UsersViewState.initial(),
    );
  },
);

class UsersViewModel extends StateNotifier<UsersViewState> {
  UsersViewModel(
    this._ref, [
    UsersViewState? usersViewState,
  ]) : super(usersViewState ?? UsersViewState.initial());

  final Ref _ref;

  // provider経由でリポジトリを取得する
  UserRepository get _userRepository => _ref.read(userRepositoryProvider);

  Future<void> fetchPage(
    int page,
    void Function(UsersResponse) onSuccess,
    void Function(String) onError,
  ) async {
    try {
      final response = await _userRepository.getUsers(page);
      onSuccess(response);
    } catch (e) {
      onError('error');
    }
  }
}


```

# テストの内容

リスト画面では、手動テストであっても表示内容と追加読み込みを確認すると思います。今回も以下の内容をテストしてみます。

1. 全1ページの場合
    1. 1つ目の要素が描画されるか
    1. 最後までスクロールして最後の要素が描画されるか
1. 複数ページの場合
    1. 2ページ目のデータの最後の要素が描画されるか

# リポジトリのmock化

[mockito](https://pub.dev/packages/mockito)でmock用クラスを生成し、メソッドの返却値を変更します。

```dart:users_view_test.dart

import 'users_view_test.mocks.dart';

@GenerateNiceMocks([
  MockSpec<UserRepository>(),
])
void main() {
  testWidgets('1st page load', (widgetTester) async {
    final mockUserRepository = MockUserRepository();
// 続く

```

## 1ページのみの場合

```dart:users_view_test.dart
    // 1
    when(mockUserRepository.getUsers(0)).thenAnswer(
      (_) async {
        return UsersResponse(
          page: 1,
          isLast: true,
          users: List.generate(
            20,
            (index) => User(
              id: index,
              name: 'test-name-$index',
            ),
          ),
        );
      },
    );
    // 2
    await widgetTester.pumpWidget(
      ProviderScope(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockUserRepository)
        ],
        child: MaterialApp(
          home: Material(
            child: UsersView(),
          ),
        ),
      ),
    );
    await widgetTester.pumpAndSettle();

    // 3
    expect(find.text('test-name-0'), findsOneWidget);

    // 4
    await widgetTester.drag(
      find.byType(PagedListView<int, User>),
      const Offset(0.0, -2000),
    );
    await widgetTester.pumpAndSettle();
    // 5
    expect(find.text('test-name-19'), findsOneWidget);
  });
```

コメントで番号を振っているので順に解説します。

1. 1ページ目のAPIリクエストの場合に`isLast=true`のデータを返却するようにmock化する
1. `ProviderScope`を利用して、`userRepositoryProvider`がmockリポジトリを返却するようにoverrideする。
1. 生成後の描画処理を待った後に、最初の要素が表示されていることを確認する
1. リストビューを十分にドラッグする
1. 画面外にあった要素が描画されるのを待った後に、最後の要素が表示されていることを確認する。

ポイントは以下の2点です

- ウィジェットテストで利用される画面は小さい。[デフォルトは800×600らしいです](https://stackoverflow.com/questions/53706569/how-to-test-flutter-widgets-on-different-screen-sizes#:~:text=It%27s%20a%20width,at%203%3A53)
- 画面外の要素は描画されない。最初これがわからず、ListTileの数をチェックしたりしていました。

## 2ページ目
1ページ目とさほど変わりませんが、1ページ目のドラッグ後に2ページ目用のフェッチを見越して再度ドラッグetcをしています

```dart
    when(mockUserRepository.getUsers(0)).thenAnswer(
      (_) async {
        return UsersResponse(
          page: 0,
          isLast: false,
          users: List.generate(
            20,
            (index) => User(
              id: index,
              name: 'test-name-$index',
            ),
          ),
        );
      },
    );
    when(mockUserRepository.getUsers(1)).thenAnswer(
      (_) async {
        return UsersResponse(
          page: 1,
          isLast: true,
          users: List.generate(
            5,
            (index) => User(
              id: index + 20,
              name: 'test-name-${index + 20}',
            ),
          ),
        );
      },
    );

    await widgetTester.pumpWidget(
      ProviderScope(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockUserRepository)
        ],
        child: MaterialApp(
          home: Material(
            child: UsersView(),
          ),
        ),
      ),
    );
    await widgetTester.pumpAndSettle();

    expect(find.byType(ListTile), findsWidgets);
    expect(find.text('test-name-0'), findsOneWidget);

    await widgetTester.drag(
      find.byType(PagedListView<int, User>),
      const Offset(0.0, -2000),
    );
    await widgetTester.pumpAndSettle();
    expect(find.text('test-name-19'), findsOneWidget);
    // 2ページ目のデータが読み込まれたはずなので再度ドラッグする
    await widgetTester.drag(
      find.byType(PagedListView<int, User>),
      const Offset(0.0, -2000),
    );

    await widgetTester.pumpAndSettle();
    expect(find.text('test-name-24'), findsOneWidget);
```

# まとめ

ページングの画面のテストを書きました。mockitoでレスポンスをmock化し、想定通りのレスポンスがAPIから返却されれば正しい振る舞いになることを確認できました。
確認が難しいエラー画面もエラー用のwidgetを作成して正しく定義すれば簡単にテストできます。
僕の経験上、一覧画面系の手動テストはデータ依存で雑になりがちなので、これを機会に自動化してみるのはいかがでしょうか

# 参考リンク

https://github.com/ham-burger/riverpod_with_infinite_scroll_pagination

https://pub.dev/packages/mockito