felangel / mocktail

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

Mocktail does not match function calls correctly when used with Riverpod and AsyncValue #200

Closed ykaito21 closed 11 months ago

ykaito21 commented 12 months ago

I have encountered an issue when using Mocktail with Riverpod and AsyncValue for testing. The problem occurs when I'm attempting to verify that a listener was called with specific arguments.

I have two identical tests. One uses Mockito and passes successfully, but the other uses Mocktail and fails with the error "No matching calls. All calls: Listener<AsyncValue<Counter>>.call(null, AsyncError<Counter>(error: Exception: test,...)".

Here are the two tests for comparison:

Test with Mocktail (fails):


class MockFetchCounter extends Mock implements FetchCounter {}

class Listener<T> extends Mock {
  void call(T? previous, T value);
}

void main() {
setUpAll(() {
  registerFallbackValue(const AsyncData(Counter(id: 1, value: 0)));
});

test('counterQueryProvider notifies listeners with AsyncError when fetchCounterProvider throws an Exception', () async {
  final container = ProviderContainer(overrides: [
    fetchCounterProvider.overrideWithValue(MockFetchCounter()),
  ]);
  final fetchCounter = container.read(fetchCounterProvider);

  // Mock fetchCounter to throw exception when called
  when(fetchCounter.call).thenThrow(Exception('test'));

  final listener = Listener<AsyncValue<Counter>>();

  // Observe a provider and listen the changes.
  container.listen(
    counterQueryProvider,
    listener.call,
    fireImmediately: true,
  );

  await expectLater(
    () => container.read(counterQueryProvider.future),
    throwsException,
  );

  verify(() => listener(null, any(that: isA<AsyncError>()))).called(1);
});
}

Test with Mockito (passes):


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

@GenerateMocks([FetchCounter, Listener])
void main() {
test('counterQueryProvider notifies listeners with AsyncError when fetchCounterProvider throws an Exception', () async {
  final container = ProviderContainer(overrides: [
    fetchCounterProvider.overrideWithValue(MockFetchCounter()),
  ]);
  final fetchCounter = container.read(fetchCounterProvider);

  // Mock fetchCounter to throw exception when called
  when(fetchCounter.call()).thenThrow(Exception('test'));
  final listener = MockListener<AsyncValue<Counter>>();

  // Observe a provider and listen the changes.
  container.listen(
    counterQueryProvider,
    listener.call,
    fireImmediately: true,
  );

  await expectLater(
    () => container.read(counterQueryProvider.future),
    throwsException,
  );

  verify(listener(argThat(isNull), argThat(isA<AsyncError>()))).called(1);
});

}

CounterQuery and FetchCounter:

class CounterQuery extends _$CounterQuery {
  @override
  Future<Counter> build() {
    return ref.watch(fetchCounterProvider).call();
  }
}

class FetchCounter {
  final CounterApi _api;

  const FetchCounter(this._api);

  Future<Counter> call() async {
      final response = await _api.getCounter(id: 1);
      return response;
  }
}

This issue seems to be specific to Mocktail, as the test passes with Mockito. I'm not sure if this is an issue with Mocktail itself or some interaction between Mocktail and Riverpod/AsyncValue.

Any help on this issue would be greatly appreciated!

carloshpb commented 11 months ago

@ykaito21 One funny thing : You are using mocktail and ended up with this situation. I have used mockito in basically the same way you did with mocktail, but with the same problem as you did have now. I'm starting to think that this is a Riverpod issue.

felangel commented 11 months ago

Hi @ykaito21 👋 Thanks for opening an issue!

Just took a look and I’m guessing the issue is due to lack of correct generic types. In the mocktail test I believe your verify should be:

verify(() => listener(null, any(that: isA<AsyncError<Counter>>()))).called(1);

The reason the mockito tests pass is because afaik mockito doesn’t support verifying with generics whereas mocktail does. Hope that helps 👍

ykaito21 commented 11 months ago

@felangel, Thanks for your comment!

verify(() => listener(null, any(that: isA<AsyncError<Counter>>()))).called(1); doesn't change the result.

https://github.com/rrousselGit/riverpod/issues/2742#issue-1806700851 might help you understand this issue. I'm not sure this is Riverpod's related issue or not.

ykaito21 commented 11 months ago

They don't support fn(null, argThat(...)). It behaves as fn(argThat(...), null) instead

Try: listener(any(that: null), any(that: isA<loading>()))

Remi helped me solve the issue https://github.com/rrousselGit/riverpod/issues/2742#issuecomment-1645551241.