RiverpodのFutureProviderを利用したUnitTestの紹介
はじめに
Riverpodの FutureProvider のテストを書いたので、メモがてら残しておこうと思います。
前提
今回はAPIリクエストし、レスポンスのJSONを記事( Article )モデルに変換して返すテストを書いていきます。そのため下記を使用します。
- APIクライアントにはhttp
- モデルにはfreezed
- JSONをモデルに変換するにはjson_serializable
- モック用ライブラリとしてmockito
- freezedとmockitoの自動生成用にbuild_runner
テスト対象のコード
今回テストするコードはこちらです。
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));
実際にテストを書いていきます。下記のテストをしています。
- 何も実行していない場合
AsyncValue.loading()になっていること fetchArticleProviderを実行した際に- 先ほど定義した
expectedArticleが返ってくること(ここはAsyncValue.data(expectedArticle)のチェックでも良いかもしれません。) mockClient.get(...)が1回呼ばれているか
- 先ほど定義した
// 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があるので、今後テストのやり方が分かり次第紹介できればと思います。