[Flutter]riverpodのStateNotifierProviderのUnitTestでハマったこと
はじめに
テスト対象のStateNotifierProviderから、テスト対象外のStateNotifierProviderのStateを参照している場合、参照しているStateをmockitoを使用してstubを作成する方法がよくわからなかったので、備忘録として残しておこうと思います。
Riverpodのバージョンが0.13.*
以前は、こちらに記載されている方法でStateをオーバライドすればできてたみたいですが、0.14.0
以降は、.state
が削除されて、この方法は使えませんでした。。。
記載しているやり方で正解なのか正直自信がないので、他に良いやり方をご存知の方はご教授いただけると嬉しいです!
環境
- Flutter version 2.2.2
- Dart version 2.13.3
依存関係
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
hooks_riverpod: ^0.14.0+1
flutter_hooks: ^0.16.0
freezed_annotation: ^0.14.1
json_serializable: ^4.1.0
meta: ^1.3.0
intl: ^0.17.0
rxdart: ^0.27.1
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.0.10
freezed: ^0.14.2
build_runner: ^2.0.6
やったこと
テスト対象のStateNotifierProvider
下記がテスト対象の実装コードです。AccountEditStateController
クラスの初期化処理で、AccountState
を読み込んで、AccountEditState
に値をセットするように実装されています。
テスト対象コード
/// AccountEditStateController provider
final accountEditStateControlProvider = StateNotifierProvider.autoDispose<
AccountEditStateController, AccountEditState>(
(ref) => AccountEditStateController(ref),
);
/// AccountEditStateController class
class AccountEditStateController extends StateNotifier<AccountEditState> {
AccountEditStateController(this._ref) : super(const AccountEditState()) {
_initialize();
}
final ProviderReference _ref;
AccountState get _accountState => _ref.read(accountStateControlProvider);
/// -------------------------------------------------------------
/// 初期処理
Future<void> _initialize() async {
final name = _accountState.name;
final value = _accountState.age;
state = state.copyWith(
nameController: TextEditingController(
text: _accountState.name,
),
ageController: TextEditingController(
text: _accountState.age,
),
);
}
}
テストコード
上記のStateNotifierProviderをテストするために記載したテストコードは下記になります。
AccountEditState
の初期値として、AccountState
の値がセットされているかチェックを行いたいので、when(mockAccountStateController.state).thenReturn(fakeAccountState);
の箇所で指定したStateを返すようにStub化を行いました。
テストコード
import 'account_edit_state_controller_test.mocks.dart';
([AccountStateController])
void main() {
final fakeAccountState = AccountState(
name: 'testName',
age: 30,
);
group('AccountEditStateControllerの確認', () {
final mockAccountStateController = MockAccountStateController();
late ProviderContainer container;
// 各テスト前にMockをセット
setUp(() {
container = ProviderContainer(
overrides: [
accountStateControlProvider
.overrideWithValue(mockAccountStateController),
],
);
});
// 各テスト前にMockを初期化
tearDown(() {
reset(mockAccountStateController);
});
test('AccountEditStateの初期値が正しいこと', () {
when(mockAccountStateController.state).thenReturn(fakeAccountState);
final state = container.read(accountEditStateControlProvider);
// 初期値の確認
expect(state.nameController?.text, fakeAccountState.name);
expect(state.ageController?.text, fakeAccountState.age);
});
}
テスト結果
上記のテストコードを実行すると下記のエラーが吐かれました。
Thrown exception:
MissingStubError: 'addListener'
No stub was found which matches the arguments of this method call:
addListener(Closure: (Object?) => void from Function '_listener@637333131':., {fireImmediately: true})
Add a stub for this method using Mockito's 'when' API, or generate the mock for MockAccountStateController with 'returnNullOnMissingStub: true'.
エラー内容を調べてみると、テスト対象のAccountEditStateController
でAccountState
を読み込んでいる、 _ref.read(accountStateControlProvider)
の部分で、addListener
が呼ばれているけど、stubが作成されてないよ〜と言った感じでした。
addListener
のstubを作成して、AccountState
を返せば良いのか!と軽い気持ちでいましたが、これのstubが全然作成できなくて挫折しました。。。
エラーメッセージのreturnNullOnMissingStub: true
をMockクラス作成のオプションに設定して、Mockの再作成なども試してみましたが、このオプションはstubを作成していない場合に、null
を返すようになるだけなので、今度はAccountState
がnull
になってしまって、こちらもエラーになってしまいました。
最終的にどうやったか
最終的にはテスト対象のAccountEditStateController
を下記のように変更することで、こっちが意図するAccountState
を渡すことができました。(テストコードは上記から変更なしです。)
最終的なテスト対象コード
/// AccountEditStateController provider
final accountEditStateControlProvider = StateNotifierProvider.autoDispose<
AccountEditStateController, AccountEditState>(
(ref) => AccountEditStateController(ref),
);
/// AccountEditStateController class
class AccountEditStateController extends StateNotifier<AccountEditState> {
AccountEditStateController(this._ref) : super(const AccountEditState()) {
_initialize();
}
final ProviderReference _ref;
+ AccountState get _accountState =>
+ _ref.read(accountStateControlProvider.notifier).state;
- AccountState get _accountState => _ref.read(accountStateControlProvider);
/// -------------------------------------------------------------
/// 初期処理
Future<void> _initialize() async {
final name = _accountState.name;
final value = _accountState.age;
state = state.copyWith(
nameController: TextEditingController(
text: _accountState.name,
),
ageController: TextEditingController(
text: _accountState.age,
),
);
}
}
おわりに
Riverpodはバージョンが0.14
以降からStateNotifierProviderの構文が変更になったので、ref.read(accountStateControlProvider)
という形でStateを読み込むことができるようになりました。しかし、Stateをスタブ化するためにref.read(accountStateControlProvider.notifier).state
としなくちゃいけないのは何か腑に落ちないので、おそらく私が方法を見つけられていないだけだと思います。。。
どなたか良い方法をご存知でしたらご教授いただけると嬉しいです!
最後まで読んでいただきありがとうございます!
Discussion