はじめに
※ この記事は以下で投稿した内容と同じです。
先日、テストを書いて実行した際に
Bad state: Cannot call when
within a stub response
というエラーが発生してテストが失敗しました。
この記事ではこの時の解決方法を解説していきます。
記事の対象者
- Flutterのテストで表題のエラーで悩んでいる方
- Riverpodを使ったDIの知識がある方
- Mockitoを使ったモックの知識がある方
- SharedPreferencesの知識がある方
記事を執筆時点での筆者の環境
[✓] Flutter (Channel stable, 3.27.1, on macOS 15.1 24B2082 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.2)
主な使用パッケージ
結論
when
で作ったスタブのthenAnswer
をちゃんと書こう!
実装
以下のような実装があったとします。
ダークモード設定が有効かどうかのフラグをローカルに保存する簡単な実装です。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:simple_base/data/repositories/app_setting/repository.dart';
part 'provider.g.dart';
@riverpod
AppSettingRepository appSettingRepository(Ref ref) => AppSettingRepository(ref);
// ignore_for_file: avoid_positional_boolean_parameters
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simple_base/data/sources/shared_preferences.dart';
class AppSettingRepository {
AppSettingRepository(this.ref);
final Ref ref;
static const isDarkModeEnabledKey = 'is_dark_mode_enabled';
Future<SharedPreferences> get _prefs =>
ref.read(sharedPreferencesProvider.future);
Future<void> setIsDarkModeEnabled(bool value) async {
final prefs = await _prefs;
await prefs.setBool(isDarkModeEnabledKey, value);
}
}
モック
mocks.dartを作成して、SharedPreferences
をモックします。
import 'package:mockito/annotations.dart';
import 'package:shared_preferences/shared_preferences.dart';
@GenerateNiceMocks([
MockSpec<SharedPreferences>(),
])
void main() {}
失敗するテスト
以下で簡単なテストを書いています。
setIsDarkModeEnabled
を実行してスタブで作ったSharedPreferences
のsetBool
が呼ばれているか検証しています。
test_1とtest_2の違いは保存している値がtrue
かfalse
かの違いだけです。
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mockito/mockito.dart';
import 'package:simple_base/data/repositories/app_setting/provider.dart';
import 'package:simple_base/data/repositories/app_setting/repository.dart';
import 'package:simple_base/data/sources/shared_preferences.dart';
import '../../../mocks.mocks.dart';
void main() {
late ProviderContainer container;
late AppSettingRepository appSettingRepository;
final sharedPreferences = MockSharedPreferences();
setUp(() {
reset(sharedPreferences);
container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
],
);
appSettingRepository = container.read(appSettingRepositoryProvider);
});
tearDown(() {
container.dispose();
});
group('setIsDarkModeEnabled', () {
const key = AppSettingRepository.isDarkModeEnabledKey;
setUp(() {
when(sharedPreferences.setBool(any, any));
});
test('test_1', () async {
await appSettingRepository.setIsDarkModeEnabled(true);
verify(sharedPreferences.setBool(key, true)).called(1);
});
test('test_2', () async {
await appSettingRepository.setIsDarkModeEnabled(false);
verify(sharedPreferences.setBool(key, false)).called(1);
});
});
}
一見よさそうに見えるこのテストですが、結果は失敗します。
正確にはテスト単体で実行すると成功するが、group
またはmain
で実行すると二つ目のテストが失敗します。
そして表題の Bad state: Cannot call when
within a stub response が発生します。
繰り返しになりますが、test_2は単体で実行すると成功します。
そこが今回の引っかかりポイントです。
解決策
今回でいうsetUp
内のwhen
で書いているスタブのthenAnswer
を書いてあげれば成功します。
setUp(() {
// 🙅 Bad
when(sharedPreferences.setBool(any, any));
// 🙆 Good
when(sharedPreferences.setBool(any, any)).thenAnswer((_) async => true);
});
SharedPreferences
のsetBool
メソッドは保存が成功したらboolを返すのですが、特に戻り値を使ってない場合は意識していないことが多いので気づきづらいです。
また、例えば戻り値がFuture<void>
であったとしても必ず.thenAnswer((_) async => {})
というようにつけなければいけないのでそこも注意が必要です。
終わりに
要はただのポカミスなのですが、単体では成功するのが曲者でした。
今回の例ではスタブが一つしかないために比較的気づきやすいですが、複数のスタブを作っている場合にその中の一つだけthenAnswer
をつけ忘れると気づきづらいです。
私は今回の原因を突き止めるまでに3時間ほど溶かしてしまいました😰
この記事が誰かのお役に立てれば幸いです。