Provider: Testing widget which are using provider

Created on 29 Jul 2019  路  14Comments  路  Source: rrousselGit/provider

Hi, i'am using the great provider package in my project.

I was about to write a widget test. And there is no documentation how to that with
provider.

I have my widget:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyViewModel>(
      builder: (BuildContext context, MyViewModel myVm,
          Widget child) { ... }

And i have my definition of providers:

runApp(
    MultiProvider(
      providers: <SingleChildCloneableWidget>[
        ChangeNotifierProvider<MyViewModel>(
            builder: (BuildContext context) => MyViewModel()),
      ],
      child: MyApp(),
    ),
  );

And i have my test:

void main() {
  testWidgets('Should work as expected',
      (WidgetTester tester) async {
    await tester.pumpWidget(MyWidget());
  });
}

How can i test my widget, with using a mock viewmodel?

Regards
Michael

documentation

Most helpful comment

You need to manually specify the object type when passed to provider:

DON'T :

Provider.value(
  value: FooMock() 
)

DO:

Provider<Foo>.value(
  value: FooMock(),
) 

All 14 comments

Hello!

You need to add the necessary providers in the widget tree passed to pumpWidget:

await tester.pumpWidget(
  Provider.value<MyViewModel>(
    value: someMock,
    child: MyWidget(), 
  ),
) 

Thank you for the quick answer!
I'am using mockito to mock my ViewModel but i get

The following ProviderNotFoundError was thrown building Consumer<MyViewModel>(dirty):
Error: Could not find the correct Provider<MyViewModel> above this
Consumer<MyViewModel> Widget

In the pumpWidgetCode i only changed Provider to ChangeNotifierProvider:

class MockMyViewModel extends Mock implements MyViewModel{}
...
Widget createWidgetForTesting({Widget child}) {
    return MaterialApp(
      home: child,
    );
  }
...
final mockVm = MockMyViewModel();
await tester.pumpWidget(ChangeNotifierProvider.value(
      value: mockVm,
      child: createWidgetForTesting(child: MyWidget()),
    ));

The createWidgetForTesting is for using MediaQuery in my widget.

How can i make i work?

You need to manually specify the object type when passed to provider:

DON'T :

Provider.value(
  value: FooMock() 
)

DO:

Provider<Foo>.value(
  value: FooMock(),
) 

Now it works! Thanks!

I'll keep this open as a reminder to document that behavior

@rrousselGit, Thanks too! I did this successfully only after finding this issue. Maybe a complete example can be documented somewhere?

BUT, I can't get my model to notify my listeners/consumers. I'm mocking the model's method that contains notifyListeners(); since it's mocked, I called the mocked model's notifyListeners method manually, however, the Consumer did not rebuild the widget.

Sample snippet in my tests

final auth = MockAuth();
when(auth.check()).thenReturn(false);
when(auth.signIn(email: email, password: password))
    .thenAnswer((_) async => FakeUser());

await tester.pumpWidget(App(
    auth: auth, // App widget builds ChangeNotifierProvider<Auth>.value(value: widget.auth,child: MaterialApp(...
    childAuth: Placeholder(),
    childGuest: OnboardingRoute(),
));

// ...log in steps here in the onboarding route since auth.check is false. A method in my widget calls auth.signIn() which contains notifyListeners() which will not trigger since it's mocked

reset(auth);
when(auth.check()).thenReturn(true);
auth.notifyListeners();

await tester.pump(); // does nothing, but I think it should still trigger from the Consumer listening to Auth/MockAuth

expect(find.byType(Placeholder), findsOneWidget); // ...MaterialApp(child: Consumer<Auth>(builder: (context, auth, child) => auth.check() ? widget.childAuth : widget.childGuest)...

Did you mock notifyListeners itself inadvertently?

I don't think so... I didn't do when(auth.notifyListeners).then...(). Here are a few more snippets for additional context.

// in tests
class MockAuth extends Mock implements Auth {}
// the auth model
class Auth extends ChangeNotifier {
  User _currentUser;

  bool check() {
    return _currentUser != null;
  }

  Future<User> signIn(
      {@required String email, @required String password}) async {
    assert(email != null);
    assert(password != null);

    // ...log in logic here

    notifyListeners();

    return _currentUser;
  }
}

Even if you didn't do when(auth.notifyListener, notifyListeners is still mocked.
Not only that, but addListeners/removeListeners are mocked too.

If you want to mock a ChangeNotifier, you can't just do extends Mock implements MyNotifier. You'll need a custom implementation.

That makes it more complicated to test my widget. Is there any easy workaround? I just need Consumer<Auth> to rebuild and call auth.check(). If not, can you give an example of a custom implementation? Did you mean a Fake instead of a Mock? That's a lot of functions to override.

You could extend the true notifier and override check only:

class AuthSpy extends Auth {
  final checkSpy = OnAuthCheck();

  @override
   bool check() => checkSpy();
}

class OnAuthCheck extends Mock {
  bool check();
}

That works. Thank you very much! For those who are interested, here's how I got my specific code to work based on my snippets above.

// on top of the test file
class SpyAuth extends Auth {
  SpyAuth({@required this.checkSpy}) : assert(checkSpy != null);

  final SpyAuthCheck checkSpy;

  @override
  bool check() => checkSpy.check();

  @override
  Future<User> signIn(
      {String email, String password}) async {
    notifyListeners();

    return User();
  }
}

class SpyAuthCheck extends Mock {
  bool check();
}
// in the actual test
final spyAuthCheck = SpyAuthCheck();
final spyAuth = SpyAuth(checkSpy: spyAuthCheck);

when(spyAuthCheck.check()).thenReturn(false);

await tester.pumpWidget(App(
    auth: spyAuth,
    childAuth: Placeholder(),
    childGuest: OnboardingRoute(),
));

// ...log in steps here

reset(spyAuthCheck);
when(spyAuthCheck.check()).thenReturn(true);

// ... tap on log in button here. it will call spyAuth.signIn()

await tester.pump(); // or pumpSettle, depending on your widget hierarchy

expect(find.byType(Placeholder), findsOneWidget); // success!

Hi, I have this issue on console:

The following ProviderNotFoundException was thrown running a test:
Error: Could not find the correct Provider<AuthProvider> above this UserInfoScreen Widget
class MockAuthProvider extends Mock implements AuthProvider {}

void main() {
  Widget makeTestableWidget({Widget child}) {
    return MaterialApp(home: child);
  }

  testWidgets('Test widget', (WidgetTester tester) async {
    final mock = MockAuthProvider();

    when(mock.getFoo()).thenReturn('Foo');

    // ASSEMBLE
    await tester.pumpWidget(
      ChangeNotifierProvider.value(
        value: mock,
        child: makeTestableWidget(
          child: UserInfoScreen(),
        ),
      ),
    );
  });
}
class UserInfoScreen extends StatefulWidget {

  @override
  _UserInfoScreenState createState() => _UserInfoScreenState();
}

class _UserInfoScreenState extends State<UserInfoScreen> with BaseWidgetState {

  AuthProvider _provider;

   @override
  void initState() {
    super.initState();
    _provider = context.read<AuthProvider>();
    final foo = _provider.getFoo();
  }
}

@MaxTenco Make sure to type your provider as ChangeNotifierProvider<AuthProvider> otherwise the type inference uses ChangeNotifierProvider<MockAuthProvider>, which isn't what you want

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rrousselGit picture rrousselGit  路  3Comments

ykaito21 picture ykaito21  路  3Comments

dikir picture dikir  路  5Comments

hou3172568 picture hou3172568  路  4Comments

tmxdyf picture tmxdyf  路  3Comments