[Flutter][Riverpod] .autoDispose使用時のunit testにおける注意点: listen()を使用する

2 min read読了の目安(約1800字

はじめに

.autoDisposeを使用すると、例えば画面遷移時にWidgetと共にそのWidgetが参照するクラス(例:ViewModel)のインスタンスも同時に破棄でき、再度画面を開いた時に初期状態で表示するということが可能になります。AndroidのViewModelに近い挙動です。

注意点

.autoDisposeによる状態破棄タイミングは、状態を購読するオブジェクトが存在しなくなった時です。これにより状態を購読しているWidgetが破棄されると、その状態(eg: ViewModel)も破棄されます。

つまりtestにおいても明示的にテスト対象クラスの状態を購読しておかないと、即座に対象クラスがdisposeされてしまいテストが失敗します。

ChangeNotifierを継承したViewModelをunit testする例で考えます。ViewModelが以下のような形で.autoDisposeを使用してDIされるとします。

final viewModelProvider = ChangeNotifierProvider.autoDispose(
    (ref) => viewModel(repository: ref.read(repositoryProvider)));

例: ProviderContainer.readでは購読にならないため即座にdisposeされる

test('ViewModel test', () async {
    final repository = MockRepository();
    when(repository.getData(any)).thenAnswer((_) async => data);
    
    final container = ProviderContainer(
      overrides: [repositoryProvider.overrideWithValue(repository)],
    );
    
    final viewModel = container.read(viewModelProvider); // 👈
    await viewModel.fetchData();
    
    expect(viewModel.data, data);
  });

以下のようなエラーとなりテストが失敗するはずです。

A ViewModel was used after being disposed.
  Once you have called dispose() on a ViewModel, it can no longer be used.

ただし、.autoDisposeを使用していない場合は上記でも動作します。

解決策

以下のようにProviderContainer.listenを呼び、そこからViewModelを取得してテストを行います。
これによりテスト終了までViewModelを破棄されることなく動作させることができます。

test('ViewModel test', () async {
    final repository = MockRepository();
    when(repository.getData(any)).thenAnswer((_) async => data);
    
    final container = ProviderContainer(
      overrides: [repositoryProvider.overrideWithValue(repository)],
    );

    final viewModel = container.listen(viewModelProvider).read(); // 👈
    await viewModel.fetchData();
    
    expect(viewModel.data, data);
  });

以上です!