dart-lang / mockito

Mockito-inspired mock library for Dart
https://pub.dev/packages/mockito
Apache License 2.0
632 stars 162 forks source link

Stubbing call with Mockito is not working with Riverpod #679

Closed carloshpb closed 1 year ago

carloshpb commented 1 year ago

When I try to test a NotifierProvider of Riverpod, I have to stub some inner providers that are used within the test class. The problem is that the methods never call the results that I've put to return with the stub.

SAMPLE CODE

@freezed
class OnboardingMessageDTO with _$OnboardingMessageDTO {
  const OnboardingMessageDTO._();

  const factory OnboardingMessageDTO({
    required String imageSvgPath,
    required String title,
    required String message,
  }) = _OnboardingMessageDTO;
}

abstract class GetOnboardingMessagesUseCase
    implements UseCase<void, List<OnboardingMessageDTO>> {
  @override
  List<OnboardingMessageDTO> execute([void request]);
}

abstract class Listener<T> {
  void call(T? previous, T next);
}

@GenerateNiceMocks([
  MockSpec<Listener>(),
  MockSpec<GetOnboardingMessagesUseCase>(),
])

final onboardingControllerProvider =
    NotifierProvider.autoDispose<OnboardingController, int>(
  () => OnboardingController(),
  name: r'onboardingControllerProvider',
);

class OnboardingController extends AutoDisposeNotifier<int> {
  late final List<OnboardingMessageDTO> _onboardingMessages;

  @override
  int build() {
    print("BUILDING CONTROLLER ...");
    _onboardingMessages =
        ref.watch(getOnboardingMessagesUseCaseProvider).execute();

    print("GOT INITIAL MESSAGES : $_onboardingMessages");

    return 0;
  }

  List<OnboardingMessageDTO> getMessages() => _onboardingMessages;

  void onPageChanged(int next) {
    if (next >= 0 && next < _onboardingMessages.length) {
      print("OK, CHANGING PAGE TO THE NEXT PAGE: $next");
      state = next;
      print("--- CURRENT STATE : $state");
    }
  }
}

void main() {
  late ProviderContainer container;
  late MockGetOnboardingMessagesUseCase getOnboardingMessagesUseCase;
  late MockListener<int> listener;
  late OnboardingController controller;

  const defaultOnboardMessages = [
    OnboardingMessageDTO(
      imageSvgPath: 'assets/images/onboarding1.svg.vec',
      title: 'SHOP CONVENIENTLY',
      message:
          'Shop from an unlimited stock of groceries from the convenience of your homes',
    ),
    OnboardingMessageDTO(
      imageSvgPath: 'assets/images/onboarding2.svg.vec',
      title: 'EXPERTLY CURATED RECIPES',
      message:
          'Our recipes are prepared in the finest of conditions by experts in their fields',
    ),
    OnboardingMessageDTO(
      imageSvgPath: 'assets/images/onboarding3.svg.vec',
      title: 'BRING OUT THE CHEF IN YOU',
      message:
          'Our recipes are specially picked so you can get cooking in no time',
    ),
  ];

  setUp(() {
    getOnboardingMessagesUseCase = MockGetOnboardingMessagesUseCase();

    // create the ProviderContainer with the mock use case
    container = ProviderContainer(
      overrides: [
        getOnboardingMessagesUseCaseProvider.overrideWithValue(
          getOnboardingMessagesUseCase,
        ),
      ],
    );

    controller = container.read(onboardingControllerProvider.notifier);

    // create a mock listener
    listener = MockListener<int>();
  });

  tearDown(() {
    container.dispose();
  });

  test('initial state is 0', () {
    // stub method to return success - no error is ever thrown
    when(getOnboardingMessagesUseCase.execute())
        .thenReturn(defaultOnboardMessages);
    // listen to the provider and call [listener] whenever its value changes
    container.listen(
      onboardingControllerProvider,
      listener,
      fireImmediately: true,
    );

    verify(getOnboardingMessagesUseCase.execute()).called(1);

    // verify
    verifyInOrder([
      listener.call(argThat(isNull), argThat(isZero)),
    ]);
    // verify that the listener is no longer called
    verifyNoMoreInteractions(listener);
  });
}

ERROR MESSAGE

00:03 +0: initial state is 0
BUILDING CONTROLLER ...
GOT INITIAL MESSAGES : []
00:03 +0 -1: initial state is 0 [E]
  Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an extension method?
  package:mockito/src/mock.dart 573:7                                                          PostExpectation._completeWhen
  package:mockito/src/mock.dart 529:12                                                         PostExpectation.thenReturn
  test\features\authentication\presentation\controllers\onboarding_controller_test.dart 76:10  main.<fn>

I've tested it in different ways (I know that storing the _onboardingMessages as an attribute in the controller class is not ideal, but it didn't work in any other way either).

Can you please guide me if I am doing something wrong here, or is this a potential issue with the way Riverpod and Mockito are interacting?

Thanks.

yanok commented 1 year ago

I have no clue what Riverpod is, sorry. Your code looks correct. But the error message doesn't match the code (it says it fails at line 76, but it also fails in when, but in your code the only when is at line 109).

Could you please make the code match the error message? It would also be great, if you could try to minimize the code. Thanks.

carloshpb commented 1 year ago

Sorry about that. The error actually occurred at the line :

verifyInOrder([
    listener.call(argThat(isNull), argThat(isZero)),
]);

I've actually changed the test code to see if I could fix it, to this :

verifyInOrder([
      listener.call(argThat(isNull), 0),
      listener.call(0, 2),
    ]);

And then the message came :

Matching call #1 not found. All calls: MockListener<int>.call(null, 0)

it always happens when I use the verify. Because the stubbed methods are not returning the correct values, like :

when(getOnboardingMessagesUseCase.execute())
        .thenReturn(defaultOnboardMessages);

The defaultOnboardMessages has some messages defined at the beginning of the test. But when calling the methods getOnboardingMessagesUseCase.execute , it always come to an empty list.

yanok commented 1 year ago

Ok, if you get an empty list from a call to getOnboardingMessagesUseCase.execute(), despite setting an expectation, it looks like your expectation doesn't work and you are getting the default dummy value (which is an empty list for List).

You can switch this mock to throw on missing stubs to verify this (MockSpec<GetOnboardingMessagesUseCase>(onMissingStub: OnMissingStub.throwException)).

Note that execute has an optional argument, so when(getOnboardingMessagesUseCase.execute()) will only match the calls to execute either without arguments or with an argument being null. If you want to stub all calls to execute, you have to use when(getOnboardingMessagesUseCase.execute(any)).

Hm.. but the argument of execute has type void... is that intentional? There might be some edge case connected to that.

yanok commented 1 year ago

@carloshpb any update? Can we close this? Looks like WAI to me.

carloshpb commented 1 year ago

@yanok sorry for the delay .... I had an urgent task to solve this week. Soon I'll look after this better and answer you.

yanok commented 1 year ago

Ok, I'm going to close it, seems to be WAI to me. Feel free to re-open with more details.

khal-it commented 2 months ago

any updates on that?