前回の記事の続きです
ページングの画面のwidgetテストをmockitoを使って書いてみます
事前準備
テスタブルなコードに改善します。前回の実装ではViewModelでリポジトリのインスタンスを生成していました。mock化できるようにリポジトリをprovider経由で取得するように変更します
user_repository.dart
final userRepositoryProvider = Provider((ref) => UserRepository());
class UserRepository {
}
ViewModel内では、provider経由でリポジトリのインスタンスを取得します。
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つ目の要素が描画されるか
- 最後までスクロールして最後の要素が描画されるか
- 複数ページの場合
- 2ページ目のデータの最後の要素が描画されるか
リポジトリのmock化
mockitoでmock用クラスを生成し、メソッドの返却値を変更します。
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ページのみの場合
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ページ目のAPIリクエストの場合に
isLast=true
のデータを返却するようにmock化する -
ProviderScope
を利用して、userRepositoryProvider
がmockリポジトリを返却するようにoverrideする。 - 生成後の描画処理を待った後に、最初の要素が表示されていることを確認する
- リストビューを十分にドラッグする
- 画面外にあった要素が描画されるのを待った後に、最後の要素が表示されていることを確認する。
ポイントは以下の2点です
- ウィジェットテストで利用される画面は小さい。デフォルトは800×600らしいです
- 画面外の要素は描画されない。最初これがわからず、ListTileの数をチェックしたりしていました。
2ページ目
1ページ目とさほど変わりませんが、1ページ目のドラッグ後に2ページ目用のフェッチを見越して再度ドラッグetcをしています
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を作成して正しく定義すれば簡単にテストできます。
僕の経験上、一覧画面系の手動テストはデータ依存で雑になりがちなので、これを機会に自動化してみるのはいかがでしょうか
参考リンク