riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Warn if a notifier mock doesn't extend a notifier base class

Open rrousselGit opened this issue 2 years ago • 15 comments
trafficstars

Bad:

class ExampleMock extends Mock implements Example

Good:

class ExampleMock extends Notifier<T> with Mock implements Example

rrousselGit avatar Mar 22 '23 08:03 rrousselGit

Hi Remi,

I am struggling with Mocking Notifiers. First because a Mockito generated Mock does not extends a Notifier base class, missing the required _setElement method.

I created a work-around but I will first make a question about your "Good" example above.

  • Question:
    When using the suggestion above, ExampleMock has no generated overrides that can be "programmed" with Mockito when. What am I missing?

I had to let Mockito generate a mock for me and copy paste all mockito overrides. A lot of stuff:

class MockPlayerTimerNotifier extends FamilyNotifier<PlayerTimer, Player> with Mock implements PlayerTimerNotifier {
  @override
  PlayerTimer build(Player arg) {
    return PlayerTimer(player: arg);
  }

  @override
  PlayerTimer get state => (super.noSuchMethod(
        Invocation.getter(#state),
        returnValue: _FakePlayerTimer(
          this,
          Invocation.getter(#state),
        ),
      ) as PlayerTimer);

  @override
  set state(PlayerTimer? value) => super.noSuchMethod(
        Invocation.setter(
          #state,
          value,
        ),
        returnValueForMissingStub: null,
      );

  @override
  bool play() => (super.noSuchMethod(
        Invocation.method(
          #play,
          [],
        ),
        returnValue: false,
      ) as bool);

  // More methods omitted.
}

class _FakePlayerTimer extends SmartFake implements PlayerTimer {
  _FakePlayerTimer(
    Object parent,
    Invocation parentInvocation,
  ) : super(
          parent,
          parentInvocation,
        );
}

The good news is that it works! But required copy/paste from Mockito code generation.
After creating the ProviderContainer with overrideWithProvider I can use:

test('Play should invoke play on the family notifier for that player', () {
  final mock = container.read(playerTimerProvider(player1).notifier);
  when(mock.play()).thenReturn(true);

  useCase.play(player1);

  verify(mock.play());
  verifyNoMoreInteractions(mock);
});

Here the work-around I implemented before seeing this post.
I subclassed my Notifier (thus extending a base notifier) as a proxy for the generated mock. Ugly? Maybe, but no mingling with copy/paste.


final mockProxyPlayerTimerNotifierProvider =
  NotifierProvider.family<MockProxyPlayerTimerNotifier, PlayerTimer, Player>(MockProxyPlayerTimerNotifier.new);

class MockProxyPlayerTimerNotifier extends PlayerTimerNotifier {
  MockPlayerTimerNotifier mock = MockPlayerTimerNotifier();

  @override
  PlayerTimer get state => mock.state;

  @override
  set state(PlayerTimer value)  => mock.state = value;

  @override
  bool play() => mock.play();

  // More methods omitted.
}

The usage is only a single line different:

  final mock = container.read(playerTimerProvider(player1).notifier).asProxy.mock;

I hope that I am missing something on your solution, that there is a right way to support Mockito when without copy/paste.

Kind regards!

cc-nogueira avatar Mar 24 '23 15:03 cc-nogueira

Do not mock the state getter/setter

rrousselGit avatar Mar 24 '23 16:03 rrousselGit

Yes sure! No need to mock protected members, it came for "free" from Mockito code generator. My bad, just need to mock the API (play, etc).

Then, would the "Good" implementation be written by hand like this?

class MockPlayerTimerNotifier extends FamilyNotifier<PlayerTimer, Player> with Mock implements PlayerTimerNotifier {
  @override
  PlayerTimer build(Player arg) {
    return PlayerTimer(player: arg);
  }

  @override
  bool play() => (super.noSuchMethod(
        Invocation.method(
          #play,
          [],
        ),
        returnValue: false,
      ) as bool);

  // More public methods omitted.
}

cc-nogueira avatar Mar 24 '23 18:03 cc-nogueira

Yes

rrousselGit avatar Mar 26 '23 10:03 rrousselGit

An alternative is https://github.com/rrousselGit/riverpod/issues/2596.

Although I'm not sure if that variant is worth the added class keyword necessary to define a notifier

rrousselGit avatar May 30 '23 09:05 rrousselGit

@rrousselGit I'm having an issue mocking a AutoDisposeFamilyAsyncNotifier generated by code-generation

the problem is that during the tests I get this error

type 'MockAlertGroupsController' is not a subtype of type 'AutoDisposeAsyncNotifier<AlertGroupsUiState>' in type cast

but this is my mock

class MockAlertGroupsController
    extends AutoDisposeFamilyAsyncNotifier<AlertGroupsUiState, AGListFilter?>
    with Mock
    implements AlertGroupsController {
  @override
  Future<AlertGroupsUiState> build(AGListFilter? filter) async {
    return const AlertGroupsUiState(alertGroups: []);
  }

  @override
  Future<void> getMoreAlertGroups() async {}
}

and I get the error whenever I call ref.watch/ref.read on the provider

imtoori avatar Jun 19 '23 09:06 imtoori

Generated notifiers need to extend the _$ base class

class MockAlertGroupsController
    extends _$AlertGroupsController
    with Mock
    implements AlertGroupsController

rrousselGit avatar Jun 19 '23 09:06 rrousselGit

Generated notifiers need to extend the _$ base class

class MockAlertGroupsController
    extends _$AlertGroupsController
    with Mock
    implements AlertGroupsController

In such a case, should the generator generate a non-private version of _$AlertGroupsController? Mocks tend to be defined in the test directory and do not have access to the file's private generated classes. The workaround is to copy the generated code to the mock.

akvus avatar Jul 28 '23 16:07 akvus

I'd define mocks next to notifiers in this case.

To begin with, now that we have keywords like "sealed"& stuff, this practice makes sense.

rrousselGit avatar Jul 28 '23 16:07 rrousselGit

Can you elaborate a little on how the sealed classes come in handy with placing mocks in the lib directory?

akvus avatar Jul 28 '23 18:07 akvus

It's not that they come in handy. But rather that keywords like sealed/final means you cannot have your mock in the test directory, as you'd get a compilation error.

rrousselGit avatar Jul 28 '23 22:07 rrousselGit

hi all, First, I'm not sure I got everything right, but this topic caused me some trouble. I'm leaving my solution here, in case you have suggestions or someone else needs it

class MockBookings extends AsyncNotifier<List<Booking>>
    with Mock
    implements Bookings {

  @override
  Future<List<Booking>> build() async {
    return super.noSuchMethod(
      Invocation.method(#build, []),
      returnValue: Future.value(<Booking>[]),
      returnValueForMissingStub: Future.value(<Booking>[]),
    );
  }
}

and in the test cases, I use (value depends on the test): when(mockBookings.build()).thenAnswer((_) async => allBookings);

janosgy avatar Aug 23 '23 16:08 janosgy

@rrousselGit to clarify when you say Mock you're referring to the mocks generated by mockito or some other one?

yoiang avatar Mar 15 '24 03:03 yoiang