felangel / mocktail

A mock library for Dart inspired by mockito
https://pub.dev/packages/mocktail
MIT License
617 stars 81 forks source link

Cubit Testing: Bad state: A test tried to use `any` or `captureAny` on a parameter of type `T State`, but registerFallbackValue was not previously called to register a fallback value for `T State` #42

Closed elianortega closed 3 years ago

elianortega commented 3 years ago

Describe the bug I'm migrating one personal project to the latest versions and null safety. But I'm having problems doing widget testing on the latest version with mocktail and cubit.

Test I'm trying to run

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

import 'package:qr_generator/qr_generator.dart';
import 'package:qr_generator_flutter/src/features/qr_generator/logic/qr_generator_cubit.dart';
import 'package:qr_generator_flutter/src/features/qr_generator/logic/qr_generator_state.dart';
import 'package:qr_generator_flutter/src/features/qr_generator/views/qr_generator_page.dart';
import 'package:qr_generator_flutter/src/features/qr_generator/views/qr_generator_page_i18n.dart';
import 'package:qr_generator_flutter/src/features/qr_scanner/logic/qr_scanner_cubit.dart';
import 'package:qr_generator_flutter/src/features/qr_scanner/logic/qr_scanner_state.dart'
    as scanner_s;
import 'package:qr_generator_flutter/src/features/qr_scanner/views/qr_scanner_page.dart';
import 'package:qr_generator_flutter/src/features/qr_generator/views/widgets/qr_code_widget.dart';

class MockGetSeed extends Mock implements GetSeed {}

class MockQrGeneratorCubit extends MockCubit<QrGeneratorState>
    implements QrGeneratorCubit {}

class MockQrScannerCubit extends MockCubit<scanner_s.QrScannerState>
    implements QrScannerCubit {}

void main() {
  group('QrGeneratorPage', () {
    final tSeed = Seed(
      seed: 'seed',
      expiresAt: DateTime.now().add(const Duration(seconds: 15)),
    );

    late QrGeneratorCubit qrGeneratorCubit;
    late QrScannerCubit qrScannerCubit;

    setUp(() {
      qrGeneratorCubit = MockQrGeneratorCubit();
      qrScannerCubit = MockQrScannerCubit();
    });

    testWidgets('renders a QrGeneratorPage', (tester) async {
      ///arrange
      when(() => qrGeneratorCubit.state)
          .thenReturn(const QrGeneratorState.initial());

      ///act
      await tester.pumpWidget(BlocProvider.value(
        value: qrGeneratorCubit,
        child: const MaterialApp(home: QrGeneratorPage()),
      ));

      ///expect
      expect(find.byKey(const Key('kBodyKey')), findsOneWidget);
    });
  });
}

Error I'm getting

//Bad state: A test tried to use `any` or `captureAny` on a parameter of type `QrGeneratorState`, but
//registerFallbackValue was not previously called to register a fallback value for `QrGeneratorState`

To fix, do:

void main() {
  setUpAll(() {
    registerFallbackValue<QrGeneratorState>(QrGeneratorState());
  });
}

//If you cannot easily create an instance of QrGeneratorState, consider defining a `Fake`:

class QrGeneratorStateFake extends Fake implements QrGeneratorState {}

void main() {
  setUpAll(() {
    registerFallbackValue<QrGeneratorState>(QrGeneratorStateFake());
  });
}

I already tried doing what the error message says, but I'm still getting the same error.

felangel commented 3 years ago

Hi @elian-ortega πŸ‘‹ Thanks for opening an issue!

As the error indicates, you need to register fallback values for the bloc event and state. You can add these following lines to your test:

class FakeQrGeneratorEvent extends Fake implements QrGeneratorEvent {}
class FakeQrGeneratorState extends Fake implements QrGeneratorState {}

void main() {
  setUpAll(() {
    registerFallbackValue<QrGeneratorEvent>(FakeQrGeneratorEvent());
    registerFallbackValue<QrGeneratorState>(FakeQrGeneratorState());
  });

  ...
}

Hope that helps πŸ‘

elianortega commented 3 years ago

It works! Thank you very much @felangel!

In my case it was only the state because I'm using cubit, so I did this to fix it:

void main() {
  setUpAll(() {
    registerFallbackValue<QrGeneratorState>(FakeQrGeneratorState());
    registerFallbackValue<scanner_s.QrScannerState>(FakeQrScannerState());
  });

  ...
}

Something more I just have one question, Why is it necessary to register the fall back value and mocking the state and event ?

image

tijanirf commented 3 years ago

Hi @felangel,

I still got an error if I use cubit that emits enum even though we already added registerFallbackValue(AppTab) (AppTab is an enum).

I can't create FakeAppTab because I can't implement enum

Screen Shot 2021-07-23 at 12 11 14 AM

Is there any workaround that does not include changing enum to FooState?

felangel commented 3 years ago

Hi @tijanirf πŸ‘‹ You can just registerFallback with a real enum value in this case:

registerFallbackValue(AppTab.home);

Hope that helps πŸ‘

shan-shaji commented 3 years ago

Hi @felangel

Can you please describe what is the use of registerFallbackValue

elianortega commented 3 years ago

Hey @shan-shaji straight from the docs:

/// Allows [any] and [captureAny] to be used on parameters of type [T].
///
/// It is necessary for tests to call [registerFallbackValue] before using
/// [any]/[captureAny] because otherwise it would not be possible to assign
/// [any]/[captureAny] as value to a non-nullable parameter.
///
/// Mocktail comes with already pre-registered values, for types such as [int],
/// [String] and more.
///
/// Once registered, a value cannot be unregistered, even when using
/// [resetMocktailState].
///
/// It is a good practice to create a function shared between all tests that
/// calls [registerFallbackValue] with various types used in the project.

In fewer word any() can substitute any basic data type: int, string, etc. But any() have no idea of the existence of a complex model from your project. So by registering a fallbackValue you give the ability to any() method to substitute the new model/data type you have registered.

shan-shaji commented 3 years ago

Thank you @elianmortega , So any() to work properly i need to register model/data type defined in my project.

elianortega commented 3 years ago

Yes, but only the ones that will be used on the tests, there is no need on registering a model that won't be used in the test πŸ‘ .

shan-shaji commented 3 years ago

Ok Thank you @elianmortega πŸ‘

noyruto5 commented 2 years ago

I have the same problem. This is my code:

class MockUserRepository extends Mock implements UserRepository {}
class MockAuthenticationBloc extends MockBloc<AuthenticationEvent, AuthenticationState> implements AuthenticationBloc {}
class FakeAuthenticationEvent extends Fake implements AuthenticationEvent {}
class FakeAuthenticationState extends Fake implements AuthenticationState {}

void main() {
  MockUserRepository mockUserRepository;
  MockAuthenticationBloc mockAuthenticationBloc;

  setUp(() {
    mockUserRepository = MockUserRepository();
    mockAuthenticationBloc = MockAuthenticationBloc();
    registerFallbackValue(FakeAuthenticationEvent());
    registerFallbackValue(FakeAuthenticationState());
  });

  group('Login', () {
    final username = 'someusername';
    final password = 'somepassword';
    final token = 'sometoken';
    final loginError = 'someerrormessage';

    blocTest('emits [LoginLoading] when successful',
      build: () {
        when(() => mockUserRepository.authenticate(username: username, password: password)).thenAnswer((_) async => token);
        return LoginBloc(userRepository: mockUserRepository, authenticationBloc: mockAuthenticationBloc);
      },
      act: (bloc) => bloc.add(LoginButtonPressed(username: username, password: password)),
      expect: () => [
        LoginInitial(),
        LoginLoading(),
      ],
    );
  });
}

And the error is: Bad state: A test tried to use any or captureAny on a parameter of type AuthenticationState, but registerFallbackValue was not previously called to register a fallback value for AuthenticationState.

To fix, do:

  void main() {
    setUpAll(() {
      registerFallbackValue(/* create a dummy instance of `AuthenticationState` */);
    });
  }

This instance of AuthenticationState will only be passed around, but never be interacted with. Therefore, if AuthenticationState is a function, it does not have to return a valid object and could throw unconditionally. If you cannot easily create an instance of AuthenticationState, consider defining a Fake:

  class MyTypeFake extends Fake implements MyType {}

  void main() {
    setUpAll(() {
      registerFallbackValue(MyTypeFake());
    });
  }

Fallbacks are required because mocktail has to know of a valid AuthenticationState to prevent TypeErrors from being thrown in Dart's sound null safe mode, while still providing a convenient syntax.