😎
【Flutter/Riverpod/Mockito】MVVM + RepositoryのUnitテストを検討する
ディレクトリ構成
lib
lib
├─ model
│ ├ user.dart
│ ├ user.freezed.dart
│ └ user.g.dart
│
├─ repository
│ ├ api_client.dart
│ └ users_repository.dart
│
├─ controller
│ ├ user_list_page_controller.dart
│ ├ user_list_page_controll.freezed.dart
│ ├ user_list_page_state.dart
│ └ user_list_page_state.freezed.dart
│
├─ page
│ └ user_list_page.dart
test
test
├─ model
│ └ user_test.dart
│
├─ repository
│ ├ users_repository_test.dart
│ └ users_repository_test.mocks.dart
│
├─ controller
│ ├ user_list_page_controller_test.dart
│ ├ user_list_page_controller_test.mocks.dart
│ ├ user_list_page_state.dart
│ └ user_list_page_state.freezed.dart
│
├─ page
│ ├ mock_user_list_page_controller.dart
│ ├ mock_user_list_page_controller.g.dart
│ └ user_list_page.dart
パッケージ
Mockito
flutter_riverpod
freezed
dio
テストコード
Model
実装コード
class User with _$User {
factory User({required int id, required String name}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
テストコード
void main() {
test("user model test", () async {
final user = User.fromJson(UserMockdata.json);
expect(user.id, 1);
expect(user.name, 'Leanne Graham');
});
}
Repository
実装コード
abstract class UsersRepositoryInterface {
Future<List<User>?> fetchUsers();
}
class UsersRepository implements UsersRepositoryInterface {
final ApiClient _apiClient;
UsersRepository({ApiClient? apiClient})
: _apiClient = apiClient ?? ApiClient();
Future<List<User>?> fetchUsers() async {
final datas = await _apiClient.get('/users');
if (datas is List<dynamic>) {
return datas.map((data) => User.fromJson(data)).toList();
}
return null;
}
}
テストコード
([MockSpec<ApiClient>()])
void main() {
test(
'user repository test',
() async {
final mockApiClient = MockApiClient();
final repository = UsersRepository(apiClient: mockApiClient);
when(mockApiClient.get('/users'))
.thenAnswer((realInvocation) async => UserMockdata.list);
expect(
await repository.fetchUsers(),
[User(id: 1, name: 'Leanne Graham')],
);
},
);
}
Controller
実装コード
class UserListPageController extends _$UserListPageController {
UserListPageController({UsersRepository? repository})
: _repository = repository ?? UsersRepository();
final UsersRepository _repository;
Future<UserListPageState> build() async {
return UserListPageState(users: await fetchUsers());
}
Future<List<User>?> fetchUsers() async {
return await _repository.fetchUsers();
}
}
テストコード
([MockSpec<UsersRepository>()])
void main() {
test(
'user list page controller test',
() async {
final UsersRepository fakeRepository = MockUsersRepository();
final controller = UserListPageController(repository: fakeRepository);
final mockData = [
User(id: 1, name: 'test1'),
User(id: 2, name: 'test2'),
User(id: 3, name: 'test3')
];
when(fakeRepository.fetchUsers())
.thenAnswer((realInvocation) async => mockData);
expect(
await controller.fetchUsers(),
mockData,
);
},
);
}
Page
実装コード
class ListPage extends ConsumerWidget {
const ListPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(userListPageControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('ユーザー一覧'),
),
body: Center(
child: data.when(
data: (state) {
final users = state.users;
if (users == null || users.isEmpty) {
return const Text('リストが空です');
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
);
},
error: (o, s) => const Text(
'データの取得に失敗しました',
style: TextStyle(color: Color.fromARGB(255, 165, 142, 140)),
),
loading: () => const CircularProgressIndicator(),
),
),
);
}
}
テストコード
void main() {
final UserListPageController fakeController = MockUserListPageController();
testWidgets('UserListPage test', (WidgetTester tester) async {
final container = ProviderContainer(
overrides: [
userListPageControllerProvider.overrideWith(() => fakeController)
],
);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const MaterialApp(
home: ListPage(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('test'), findsOneWidget);
expect(find.text('ユーザー一覧'), findsOneWidget);
});
}
モックControllerを別ファイルで定義する。
part 'mock_user_list_page_controller.g.dart';
class MockUserListPageController extends _$MockUserListPageController
with Mock
implements UserListPageController {
Future<UserListPageState> build() async {
return UserListPageState(users: [User(id: 1, name: 'test')]);
}
}
解説
model
final user = User.fromJson(UserMockdata.json);
モックデータをfromJsonメソッドに渡す。
expect(user.id, 1);
expect(user.name, 'Leanne Graham');
jsonをコンバートできるか確認する。
repository
([MockSpec<ApiClient>()])
ApiClientのモックを自動生成する。
final mockApiClient = MockApiClient();
final repository = UsersRepository(apiClient: mockApiClient);
モックApiClientをRepositoryにセットする。
when(mockApiClient.get('/users'))
.thenAnswer((realInvocation) async => UserMockdata.list);
モックApiClientのレスポンスを定義する。
expect(
await repository.fetchUsers(),
[User(id: 1, name: 'Leanne Graham')],
);
fetchUsersをテスト
レスポンスが一致するか確認する。
controller
([MockSpec<UsersRepository>()])
Repositoryのモックを自動生成する。
final UsersRepository fakeRepository = MockUsersRepository();
final controller = UserListPageController(repository: fakeRepository);
モックRepositoryをControllerにセットする。
final mockData = [
User(id: 1, name: 'test1'),
User(id: 2, name: 'test2'),
User(id: 3, name: 'test3')
];
when(fakeRepository.fetchUsers())
.thenAnswer((realInvocation) async => mockData);
モックRepositoryのfetchUsersのレスポンスを定義する。
expect(
await controller.fetchUsers(),
mockData,
);
fetchUsersをテストする。
レスポンスが一致するか確認する。
page
part 'mock_user_list_page_controller.g.dart';
class MockUserListPageController extends _$MockUserListPageController
with Mock
implements UserListPageController {
Future<UserListPageState> build() async {
return UserListPageState(users: [User(id: 1, name: 'test')]);
}
}
riverpod generatorで作成したcontrollerはモックの自動生成ができないので
別ファイルにモックcontrollerを作成する。
final UserListPageController fakeController = MockUserListPageController();
モックControllerを初-
final container = ProviderContainer(
overrides: [
userListPageControllerProvider.overrideWith(() => fakeController)
],
);
ProviderContainerを作成する。
モックControllerでオーバーライドする。
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const MaterialApp(
home: ListPage(),
),
),
);
```dart
UncontrolledProviderScopeでMaterialAppをラップする。
ProviderContainerをセットする。
```dart
await tester.pumpAndSettle();
非同期処理の結果を待つ。
expect(find.text('test'), findsOneWidget);
expect(find.text('ユーザー一覧'), findsOneWidget);
ユーザー名のtest
が表示されることを確認する。
タイトルのユーザー一覧
が表示されることを確認する。
Discussion