😽

[Riverpod]FutureProvider内でStateNotifierをwatchする際のテスト用モック作成のメモ

2022/01/23に公開

はじめに

モッククラスの作成が全然うまくいかなかったので、メモ。
※テストの知見深いわけではないので、おかしいところあればご指摘お願いします🙇‍♂️

↓こちらのリポジトリを使用
https://github.com/oke331/memo_sample/tree/develop

環境

  • Flutter 2.8.1
  • flutter_hooks 0.18.0
  • hooks_riverpod 1.0.3
  • mockito 5.0.17

やったこと

下記のメモ情報を取得してくる memoProvider をテストしようとした。

final memoProvider =
    FutureProvider.family.autoDispose<Memo?, String?>((ref, memoId) async {
  // memoIdがnullの場合はnullを返却
  if (memoId == null) {
    return null;
  }

  // user情報をwatchしてidを取得
  final user =
      ref.watch(authControllerProvider.select((value) => value.firebaseUser));
  
  // userIdとmemoIdからメモ情報を取得
  return ref.read(memoRepositoryProvider).fetchMemo(
        userId: user.value!.uid,
        memoId: memoId,
      );
});
authControllerの実装はこちら
final authControllerProvider = StateNotifierProvider<AuthController, AuthState>(
  (ref) => AuthController._(FirebaseAuth.instance),
);

class AuthController extends StateNotifier<AuthState> {
  AuthController._(
    this._auth,
  ) : super(AuthState()) {
    _auth.authStateChanges().listen(
      (user) {
        state = state.copyWith(
          firebaseUser: AsyncValue.data(user),
        );
      },
    );
  }

  final FirebaseAuth _auth;

  Future<void> signOut() => _auth.signOut();
}


class AuthState with _$AuthState {
  factory AuthState({
    (AsyncValue<User?>.loading()) AsyncValue<User?> firebaseUser,
  }) = _AuthState;
  AuthState._();

  late final bool hasAlreadySignedIn = firebaseUser.value != null;
}


上記の memoProvider に対して、こんな感じのテストを組んでいた。(mockitパッケージを使用しています)

mockクラスの作成

下記クラスを flutter pub run build_runner build でMockクラスを作成。

test/mocks/generate_mocks.dart
([
  Memo,
  MemoRepository,
  User,
  AuthState,
  AuthController,
])
void main() {}
void main() {
  group('[memoProvider]', () {
    // memoIdがnullの時nullが返ってくるかテスト
    // こっちはOK
    group('when the memoId is set to null', () {
      test('return null', () async {
        const settingValue = null;
        final container = ProviderContainer(overrides: [
          memoRepositoryProvider.overrideWithValue(MockMemoRepository()),
        ]);

        expect(
          container.read(memoProvider(settingValue)),
          const AsyncValue<Memo?>.loading(),
        );

        await container.read(memoProvider(settingValue).future);

        expect(
          container.read(memoProvider(settingValue)),
          const AsyncData<Memo?>(null),
        );
      });
    });

    // memoIdとuserIdがセットされている時Memoオブジェクトが返却されるかテスト
    // こっちはNG
    group('when the memoId is set to "1" and the userId is set to "test"', () {
      test('return Memo object', () async {
        const settingValue = '1';
        final result = MockMemo();

        final mockMemoRepository = MockMemoRepository();
        final mockMemoRepository = MockMemoRepository();
        when(
          mockMemoRepository.fetchMemo(
            userId: anyNamed('userId'),
            memoId: anyNamed('memoId'),
          ),
        ).thenAnswer((_) async => result);

        final mockUser = MockUser();
        when(mockUser.uid).thenReturn('test');
        final mockAuthState = MockAuthState();
        when(mockAuthState.firebaseUser).thenReturn(AsyncData(mockUser));
        final mockAuthController = MockAuthController();
	when(mockAuthController.state).thenReturn(mockAuthState);

        final container = ProviderContainer(overrides: [
          memoRepositoryProvider.overrideWithValue(mockMemoRepository),
          authControllerProvider.overrideWithValue(mockAuthController),
        ]);

        expect(
          container.read(memoProvider(settingValue)),
          const AsyncValue<Memo?>.loading(),
        );

        await container.read(memoProvider(settingValue).future);

        expect(
          container.read(memoProvider(settingValue)),
          AsyncData<Memo?>(result),
        );
      });
    });
  });
}

しかし、このコードだと二つ目の group のテストで下記のエラーでうまく動かない。

  Thrown exception:
  MissingStubError: 'addListener'
  No stub was found which matches the arguments of this method call:
  addListener(Closure: (AuthState) => void, {fireImmediately: true})

エラーが出ていたのが memoProvider の下記の箇所。

  final user =
      ref.watch(authControllerProvider.select((value) => value.firebaseUser));

エラー内容から addListner メソッドのstubがないと言われているので、whenで実装しようとしてもうまくいかない。

そのため、下記の StateNotifierAuthController のモッククラスを作成。

test/mocks/mocks.dart
class MockStateNotifier<T> extends StateNotifier<T> with Mock {
  MockStateNotifier(T state) : super(state);
}

class MockAuthController extends MockStateNotifier<AuthState>
    implements AuthController {
  MockAuthController(AuthState authState) : super(authState);
}

テストを下記のように書き換えると成功した。

 group('when the memoId is set to "1" and the userId is set to "test"', () {
      test('return Memo object', () async {
        const settingValue = '1';
        final result = MockMemo();

        final mockMemoRepository = MockMemoRepository();
        final mockMemoRepository = MockMemoRepository();
        when(
          mockMemoRepository.fetchMemo(
            userId: anyNamed('userId'),
            memoId: anyNamed('memoId'),
          ),
        ).thenAnswer((_) async => result);

        final mockUser = MockUser();
        when(mockUser.uid).thenReturn('test');
        final mockAuthState = MockAuthState();
        when(mockAuthState.firebaseUser).thenReturn(AsyncData(mockUser));
-       final mockAuthController = MockAuthController();
-       when(mockAuthController.state).thenReturn(mockAuthState);
+       final mockAuthController = MockAuthController(mockAuthState);        

        final container = ProviderContainer(overrides: [
          memoRepositoryProvider.overrideWithValue(mockMemoRepository),
          authControllerProvider.overrideWithValue(mockAuthController),
        ]);

        expect(
          container.read(memoProvider(settingValue)),
          const AsyncValue<Memo?>.loading(),
        );

        await container.read(memoProvider(settingValue).future);

        expect(
          container.read(memoProvider(settingValue)),
          AsyncData<Memo?>(result),
        );
      });
    });

zennに一度投稿してみたくてメモ程度に投稿しました。

StateNotifierの中身のコードの理解ができていない。勉強せねば(泣)

Discussion