【Unit Test】Bad state: Cannot call `when` within a stub response

## はじめに

※ この記事は以下で投稿した内容と同じです。

https://zenn.dev/harx/articles/d9b476571a2a6b

先日、テストを書いて実行した際に

**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)
```

### 主な使用パッケージ

https://pub.dev/packages/riverpod

https://pub.dev/packages/mockito

https://pub.dev/packages/shared_preferences

## 結論

**`when`で作ったスタブの`thenAnswer`をちゃんと書こう！**

## 実装

以下のような実装があったとします。
ダークモード設定が有効かどうかのフラグをローカルに保存する簡単な実装です。

```dart
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);

```

```dart
// 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`をモックします。

```dart
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`かの違いだけです。

```dart
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`を書いてあげれば成功します。

```dart
    setUp(() {
      // 🙅 Bad
      when(sharedPreferences.setBool(any, any));

      // 🙆 Good
      when(sharedPreferences.setBool(any, any)).thenAnswer((_) async => true);
    });
```

`SharedPreferences`の`setBool`メソッドは保存が成功したらboolを返すのですが、特に戻り値を使ってない場合は意識していないことが多いので気づきづらいです。

また、例えば戻り値が`Future<void>`であったとしても必ず`.thenAnswer((_) async => {})`というようにつけなければいけないのでそこも注意が必要です。

## 終わりに

要はただのポカミスなのですが、単体では成功するのが曲者でした。
今回の例ではスタブが一つしかないために比較的気づきやすいですが、複数のスタブを作っている場合にその中の一つだけ`thenAnswer`をつけ忘れると気づきづらいです。

私は今回の原因を突き止めるまでに３時間ほど溶かしてしまいました😰

この記事が誰かのお役に立てれば幸いです。