☠️

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

2025/02/22に公開

はじめに

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

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をちゃんと書こう!

実装

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

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';


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';

([
  MockSpec<SharedPreferences>(),
])
void main() {}

失敗するテスト

以下で簡単なテストを書いています。
setIsDarkModeEnabledを実行してスタブで作ったSharedPreferencessetBoolが呼ばれているか検証しています。
test_1とtest_2の違いは保存している値がtruefalseかの違いだけです。

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);
    });

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

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

終わりに

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

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

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

Discussion