Search code examples
fluttermockitoriverpod

I don't know how to mock Riverpod's Provider with mockito


I am writing a test in Flutter using mockito, I want to mock Riverpod's Provider, but I get an error and can't solve it.

The various versions are as follows

# pubspec.yaml
environment:
  sdk: ^3.5.2

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5
  freezed_annotation: ^2.4.4

dev_dependencies:
  flutter_test:
    sdk: flutter
  riverpod_generator: ^2.4.3
  riverpod_lint: ^2.3.13
  freezed: ^2.5.7
  mockito: ^5.4.4

Here is the code we want to test

// notifier_provider_page.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_flutter/user_item_provider.dart';

import 'todo.dart';

part 'notifier_provider_page.g.dart';

@riverpod
class TodoList extends _$TodoList {
  @override
  List<Todo> build() {
    return [];
  }

  // Methods under test
  void add(Todo todo) {
    final user = ref.read(userItemProvider);
    if (user?.email == 'a') {
      state = [...state, todo];
    }
  }
}

The userItemProvider used in the above code is as follows

// user_item_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'user.dart';

part 'user_item_provider.g.dart';

@riverpod
class UserItem extends _$UserItem {
  @override
  User build() {
    return const User(name: "a", email: "b", password: "c");
  }

  void add(User user) {
    state = user;
  }
}

The test code is as follows (it fails)

// notifier_provider_page_test.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:riverpod_flutter/notifier_provider_page.dart';
import 'package:riverpod_flutter/todo.dart';
import 'package:riverpod_flutter/user_item_provider.dart';

import 'notifier_provider_page_test.mocks.dart';

@GenerateNiceMocks([MockSpec<UserItem>()])
void main() {
  late ProviderContainer container;
  final MockUserItem mockUserItem = MockUserItem();

  setUp(() {
    container = ProviderContainer(overrides: [
      userItemProvider.overrideWith(() {
        return mockUserItem;
      })
    ]);
  });

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

  test('test', () {
    final todo = container.read(todoListProvider.notifier);

    todo.add(const Todo(
      userId: 1,
      id: 1,
      title: 'title',
    ));

    expect(container.read(todoListProvider),
        [const Todo(userId: 1, id: 1, title: 'title')]);
  });
}

The following error occurs

NoSuchMethodError: Class 'MockUserItem' has no instance method '_setElement'.
Receiver: Instance of 'MockUserItem'
Tried calling: _setElement(Instance of 'AutoDisposeNotifierProviderElement<UserItem, User>')

The code modified from this page is as follows

// user_item_provider.dart

// The following classes were added
class MockUserItem extends AutoDisposeNotifier<User>
    with Mock
    implements UserItem {}

Commented out the next two lines

// notifier_provider_page_test.dart

// import 'notifier_provider_page_test.mocks.dart';
// @GenerateNiceMocks([MockSpec<UserItem>()])

In this case, the following error occurs The official documentation of mockito is available, but this does not solve the problem because the return value of the build method must be changed.

type 'Null' is not a subtype of type 'User'

Is there a way to test without changing the real class?


Solution

  • The beauty of Riverpod is you do not need to Mock the Providers/Notifiers. You can create stubs with ease.

    I am not familiar with the inner workings of Riverpod Code Generation so this example may not match one to one and may require some tweaking. From reading your code examples, it looks to be a Notifier.

    Here is an example of how to Stub a Notifier for use in a test without any mocking libraries.

    Say we have a Notifier called UserItemNotifier that returns a UserItem from its build method. To create a stub we can create a new class in the test called UserItemNotifierStub. The _initialValue in the constructor allows you to pass in any UserItem so that you can test multiple scenarios.

    class UserItemNotifierStub extends UserItemNotifier {
      MyNotifierStub(this._initialValue);
    
      final UserItem _initialValue;
    
      @override
      UserItem build() => _initialValue;
    }
    

    You can then use this Stub in your test like so:

    late ProviderContainer container;
    const user = const User(name: "a", email: "b", password: "c")
      setUp(() {
        container = ProviderContainer(overrides: [
          userItemProvider.overrideWith(() => UserItemNotifierStub(user))
        ]);
      });
    

    I hope this helps understand how to use Riverpod in tests.