🤖

RiverpodのNotifierで作ったViewModelをmockitoでAPIをモック化してテストする

2024/01/23に公開

FlutterRiverpodNotifierで作ったViewModelにおいて、APIリクエストを含むViewModelのメソッドを、mockitoでAPIをモック化することによりテストしてみたので、最小のコード例を紹介します。

参考文献

https://codewithandrea.com/articles/unit-test-async-notifier-riverpod/

アプリ側コード

  • dart run build_runner buildは適宜実行

API

  • dio
  • retrofit

GithubSearchApi githubSearchApi(GithubSearchApiRef ref) {
  return GithubSearchApi(ref.watch(githubDioProvider));
}

()
abstract class GithubSearchApi {
  factory GithubSearchApi(Dio dio) = _GithubSearchApi;

  ('/search/repositories')
  Future<GithubSearchRepositoriesResponse> searchRepositories(
    ('q') String searchKeyword,
  );
}

viewModel


class HomeTabViewModel extends _$HomeTabViewModel {
  
  HomeTabState build() => const HomeTabState(repositories: []);

  GithubSearchApi get _githubSearchApi => ref.watch(githubSearchApiProvider);

  Future<void> search(String searchWord) async {
    final response = await _githubSearchApi.searchRepositories(searchWord);
    state = state.copyWith(repositories: response.items);
  }
}


class HomeTabState with _$HomeTabState {
  const factory HomeTabState({
    ([]) List<GithubRepository> repositories,
  }) = _HomeTabState;
}

テストコード

([MockSpec<GithubSearchApi>()])
void main() {
  ProviderContainer makeProviderContainer(MockGithubSearchApi githubSearchApi) {
    final container = ProviderContainer(
      overrides: [githubSearchApiProvider.overrideWithValue(githubSearchApi)],
    );
    addTearDown(container.dispose);
    return container;
  }

  group('home_tab_view_model', () {
    test('search', () async {
      final githubSearchApi = MockGithubSearchApi();
      final searchResultRepositories = [
        GithubRepository(
            name: 'repository-1',
            fullName: 'repository-1',
            language: 'lang',
            stargazersCount: 1,
            watchersCount: 2,
            forksCount: 3,
            issuesCount: 4)
      ];

      when(githubSearchApi.searchRepositories('repo')).thenAnswer((_) {
        return Future.value(GithubSearchRepositoriesResponse(
          totalCount: 1,
          imcompleteResults: false,
          items: searchResultRepositories,
        ));
      });

      final container = makeProviderContainer(githubSearchApi);

      container.listen(homeTabViewModelProvider, (previous, next) {
        debugPrint('');
      }, fireImmediately: true);

      // メソッドを実行する前のStateをテスト
      const expectedStateBeforeSearch = HomeTabState(repositories: []);
      final actualStateBeforeSearch = container.read(homeTabViewModelProvider);

      expect(actualStateBeforeSearch, expectedStateBeforeSearch);

      final homeTabViewModel =
          container.read(homeTabViewModelProvider.notifier);

      // viewModelのメソッドを実行
      await homeTabViewModel.search('repo');

      // メソッドを実行した後のStateを確認
      final expectedStateAfterSearch =
          HomeTabState(repositories: searchResultRepositories);
      final actualStateAfterSearch = container.read(homeTabViewModelProvider);

      expect(actualStateAfterSearch, expectedStateAfterSearch);
    });
  });
}

Discussion