2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Riverpodのテストコードを書こう # vol.1

2
Last updated at Posted at 2025-12-27

前書き

本日は【札幌現地+オンライン開催】力強くブログを108記事アウトプットというイベントで拘束されに来ています。とりあえず1本は書かないと脱走できそうにないので今年の自分の振り返りとして苦労したことをまとめたいと思います。

Riverpod

RiverpodはFlutterでの状態管理及びDIのプラグインです。ここ数年でかなりスタンダードな存在になりました。私は前身のProviderから使っておりますが、より洗練された存在になりました。
数ヶ月前にv3がメジャーリリースとなり、マイグレーション作業に少々苦心したのはまるで昨日のことのようです。

非同期系Provider

RiverpodにはFutureProviderやStreamProviderという種類のProviderがあります。こちらは公式ドキュメントに存在するサンプルコードです。ユーザーのAPIを叩いてそれをUser型に変換しているようですね。

user_provider.dart
// The equivalent of our fetchUser function, but the result is cached.
// Using userProvider multiple times will return the same value.
final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get('https://api.example.com/user/123');
  return User.fromJson(response.body);
});

Dependency Injection(DI)とMock

こんな感じのコードですが実際には直接何らかのプラグインを実行するより、httpクライアントの処理を抽象化してDIすることが多いはずです。そうでないと意図したテストケースを用意するが難しいでしょう。以下は先ほどのhttpのリクエストをRestClientとして抽象化し、それをProviderで提供する例です。

rest_client.dart
abstract interface class RestClient<T> {
  Future<T> get(String url, {Map<String, dynamic>? queryParameters});
  Future<T> post(String url, {Object? body});
  ...
rest_client_provider.dart
final restClientProvider = Provider<RestClient<Response>>((ref) {
  final dio = Dio();
  return DioClient(dio: dio); // httpの実装はDioプラグインを使う
});

最終的にDI対応したuserProviderはこのようになります。

user_provider.dart
final userProvider = FutureProvider<User>((ref) async {
  final client = ref.watch(restClientProvider);
  final response = await client.get('https://api.example.com/user/123');
  return User.fromJson(data.toString());
});

これによりテストのときはモックと差し替えることができます。どうやるかというとシナリオに応じたスタブにすることで意図したテストケースが記述できるようになります。モックはmockitoなどで自動作成すると楽チンです。

user_provider_test.dart
void main() {
  group('ユーザー', () {
    late MockRestClient mockRestClient;

    setUp(() {
      mockRestClient = MockRestClient();
    });

    tearDown(() {
      reset(mockRestClient);
    });

    when('ハッピーパス', () async {
      final mockResponse = MockResponse();
      when(mockRestClient.get('https://api.example.com/user/123').thenAnswer(
        (_) async => mockResponse,
      );
      when(mockResponse.body.thenReturn('''
{
  "id": "test_user_id_1234",
  "name": "alice",
  "age": 20
}'''
      ...
    });

非同期Providerのテスト

非同期のProviderはFutureProviderとStreamProviderがあります。

FutureProvider

まずはFutureProviderです。公式ドキュメントUnit Testに書いている通りにDIを行い、モックで定義したテストケースの検証をしましょう。ここでは「レスポンスボディのjsonをUser型にパースできているか」をハッピーパスとしています。(実際にはもっといい名前を付けたほうがいいでしょう...)

user_provider_test.dart
  /* 先ほどの続き */
  ...
  final container = ProviderContainer.test(
    overrides: [
      restClientProvider.overrideWith((_) => mockRestClient),
    ],
  );
  addTearDown(container.dispose);
  final user = await container.read(userProvider.future);
  expect(user.id, 'test_user_id_1234');
  expect(user.name, 'Alice');

StreamProvider

問題はStreamProviderです。実際にStreamProviderのテストをどう書いたら良いのかが公式ドキュメントに載っていません...

Testing your providers
https://riverpod.dev/docs/how_to/testing

Provider, FutureProviderのoverrideやUIテストの書き方があるのですが、StreamProviderのサンプルコードはありません。実はRiverpodのGitHubリポジトリにてDiscussionが提起され、作者から解決策が示されています。

ポイントは以下です。

  1. container.listenを使用する
  2. await container.pump()でstreamの値が更新されるのを待つ
sample_stream_provider_test.dart
void main() {
  group('StreamProvider', () {
    test(
      'stream providerの値を確認する',
      () async {
        final container = ProviderContainer.test(
          overrides: [],
        );
        addTearDown(container.dispose);

        final sub = container.listen(
          testStreamProvider.future,
          (_, n) async {
            // Spy
          },
          fireImmediately: true,
        );

        final first = await sub.read();
        expect(first, 1);

        await container.pump();

        final second = await sub.read();
        expect(second, 2);
      },
    );
  });
}

final testStreamProvider = StreamProvider.autoDispose((_) async* {
  yield* Stream.fromIterable([1, 2]).asBroadcastStream();
});

まとめ

Riverodのテストコードで一部のProviderのテストコードのベストプラクティスが公式ドキュメントのわかりやすい箇所になくて結構困りました。他にもNotifierProviderには推奨されてない記述があります。そちらは# vol.2に続けます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?