RiverpodのFutureProviderを利用したUnitTestの紹介

はじめに

Riverpodの FutureProvider のテストを書いたので、メモがてら残しておこうと思います。

前提

今回はAPIリクエストし、レスポンスのJSONを記事( Article )モデルに変換して返すテストを書いていきます。そのため下記を使用します。

テスト対象のコード

今回テストするコードはこちらです。 APIを叩くと下記のJSONを取得できるので、それを Article モデルに変換するコードです。

レスポンスのJSON

{
  'title': '銀河鉄道の夜',
  'description': '誰だって、ほんとうにいいことをしたら、いちばん幸せなんだねぇ'
}

Articleモデル

APIのレスポンスを Article モデルに変換します。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'article.freezed.dart';
part 'article.g.dart';

@freezed
class Article with _$Article {
  @JsonSerializable(fieldRename: FieldRename.snake)
  const factory Article({
    required String title,
    required String description,
  }) = _Article;

  factory Article.fromJson(Map<String, dynamic> json) => _$ArticleFromJson(json);

  const Article._();
}

APIを叩きレスポンスをArticleモデルに変換する部分

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'fetch_article.g.dart'

@riverpod
http.Client httpClient(Ref ref) => http.Client();

@riverpod
Future<Article> fetchArticle(Ref ref, {required String url}) async {
  final client = ref.read(httpClientProvider);
  final response = await client.get(Uri.parse(url));

  if (response.statusCode == 200) {
    return Article.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('failed to fetch data');
  }
}

テストコードを書いていく

全体のコード

@GeneratedNiceMocks(
  MockSpec<http.Client>(),
  MockSpec<http.Response>(),
)
void main() {
  group('fetchArticle', () {
    test('should return Article', () {
      // テストに必要なデータを作成
      const url = 'https://idonuntius/article/';
      final responseBody = {
        'title': '銀河鉄道の夜',
        'description': '誰だって、ほんとうにいいことをしたら、いちばん幸せなんだねぇ',
      };
      final expectedArticle = Article(
        title: '銀河鉄道の夜',
        description: '誰だって、ほんとうにいいことをしたら、いちばん幸せなんだねぇ',
      );

      // mock
      final mockClient = MockClient();
      final mockResponse = MockResponse();

      // ProviderContainerの作成
      final container = ProviderContainer(overrides:
        [
          httpClientProvider.overrideWithValue(mockClient),
        ],
      );
      addTearDown(container.dispose);

      // stub
      when(mockResponse.statusCode).thenReturn(200);
      when(mockResponse.body).thenReturn(responseBody);
      when(mockClient.get(Uri.parse(url))).thenAnswer((_) => Future.value(responseBody));

      // test
      expect(
        container.read(fetchArticleProvider(url: url)),
        const AsyncValue<Article>.loading(),
      );
      await container.read(fetchArticleProvider(url: url).future);
      expect(
        container.read(fetchArticleProvider(url: url)).value,
        expectedArticle,
      );
      verify(mockClient.get(Uri.parse(url))).called(1);
    });
  });
}

詳細

まずモックの生成にmockitoの @GeneratedNiceMocks を使用します。(アノテーションの記載後build_runnerを実行する必要があります。)

@GeneratedNiceMocks(
  MockSpec<http.Client>(),
  MockSpec<http.Response>(),
)

テストで使用するデータを作成します。

// テストに必要なデータを作成
const url = 'https://idonuntius/article/';
final responseBody = {
  'title': '銀河鉄道の夜',
  'description': '誰だって、ほんとうにいいことをしたら、いちばん幸せなんだねぇ',
};
final expectedArticle = Article(
  title: '銀河鉄道の夜',
  description: '誰だって、ほんとうにいいことをしたら、いちばん幸せなんだねぇ',
);

Riverpodの ProviderContainer を利用して、 httpClientProvider が保持しているものを mockClient に書き換えます。

// ProviderContainerの作成
final container = ProviderContainer(overrides:
  [
    httpClientProvider.overrideWithValue(mockClient),
  ],
);
addTearDown(container.dispose);

ちなみに公式ドキュメントにも記載されていますが、下記のようなProviderContainerを生成するメソッドを作っておくと使い回しができ便利です。

ProviderContainer createContainer({
  ProviderContainer? parent,
  List<Override> overrides = const [],
  List<ProviderObserver>? observers,
}) {
  final container = ProviderContainer(
    parent: parent,
    overrides: overrides,
    observers: observers,
  );
  addTearDown(container.dispose);
  return container;
}

生成したモッククラスを元にスタブします。

// stub
when(mockResponse.statusCode).thenReturn(200);
when(mockResponse.body).thenReturn(responseBody);
when(mockClient.get(Uri.parse(url))).thenAnswer((_) => Future.value(responseBody));

実際にテストを書いていきます。下記のテストをしています。

 // test
 expect(
   container.read(fetchArticleProvider(url: url)),
   const AsyncValue<Article>.loading(),
 );
 await container.read(fetchArticleProvider(url: url).future);
 expect(
   container.read(fetchArticleProvider(url: url)).value,
   expectedArticle,
 );
 verify(mockClient.get(Uri.parse(url))).called(1);

最後に

mockitoを用いたFutureProviderのテストに紹介しました。
RiverpodにはさまざまなProviderがあるので、今後テストのやり方が分かり次第紹介できればと思います。