前書き
本日は【札幌現地+オンライン開催】力強くブログを108記事アウトプットというイベントで拘束されに来ています。とりあえず1本は書かないと脱走できそうにないので今年の自分の振り返りとして苦労したことをまとめたいと思います。
Riverpod
RiverpodはFlutterでの状態管理及びDIのプラグインです。ここ数年でかなりスタンダードな存在になりました。私は前身のProviderから使っておりますが、より洗練された存在になりました。
数ヶ月前にv3がメジャーリリースとなり、マイグレーション作業に少々苦心したのはまるで昨日のことのようです。
非同期系Provider
RiverpodにはFutureProviderやStreamProviderという種類のProviderがあります。こちらは公式ドキュメントに存在するサンプルコードです。ユーザーのAPIを叩いてそれをUser型に変換しているようですね。
// 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で提供する例です。
abstract interface class RestClient<T> {
Future<T> get(String url, {Map<String, dynamic>? queryParameters});
Future<T> post(String url, {Object? body});
...
final restClientProvider = Provider<RestClient<Response>>((ref) {
final dio = Dio();
return DioClient(dio: dio); // httpの実装はDioプラグインを使う
});
最終的にDI対応したuserProviderはこのようになります。
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などで自動作成すると楽チンです。
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型にパースできているか」をハッピーパスとしています。(実際にはもっといい名前を付けたほうがいいでしょう...)
/* 先ほどの続き */
...
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が提起され、作者から解決策が示されています。
ポイントは以下です。
-
container.listenを使用する await container.pump()でstreamの値が更新されるのを待つ
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に続けます。