简体   繁体   中英

How to unit test whether the ChangeNotifier's notifyListeners was called in Flutter/Dart?

I'm using the provider package in our app and I want to test my ChangeNotifier class individually to have simple unit tests checking the business logic.

Apart from the values of ChangeNotifier properties, I also want to ensure that in certain cases (where necessary), the notifyListeners has been called, as otherwise, the widgets that rely on up-to-date information from this class would not be updated.

Currently, I'm indirectly testing whether the notifyListeners have been called: I'm using the fact that the ChangeNotifier lets me add a callback using its addListener method. In the callback that I add in our testing suite, I simply increment an integer counter variable and make assertions on that.

Is this the right way to test whether my ChangeNotifier calls its listeners? Is there a more descriptive way of testing this?

Here is the class I'm testing (simplified, so I can share it on StackOverflow):

import 'package:flutter/foundation.dart';

class ExampleModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
    notifyListeners();
  }
}

and this is how I test it:

import 'package:mobile_app/example_model.dart';
import 'package:test/test.dart';

void main() {
  group('$ExampleModel', () {
    ExampleModel exampleModel;
    int listenerCallCount;

    setUp(() {
      listenerCallCount = 0;
      exampleModel = ExampleModel()
        ..addListener(() {
          listenerCallCount += 1;
        });
    });

    test('increments value and calls listeners', () {
      exampleModel.increment();
      expect(exampleModel.value, 1);
      exampleModel.increment();
      expect(listenerCallCount, 2);
    });

    test('unit tests are independent from each other', () {
      exampleModel.increment();
      expect(exampleModel.value, 1);
      exampleModel.increment();
      expect(listenerCallCount, 2);
    });
  });
}

Also, if you think testing this differently would be better, please let me know, I'm currently working as a solo Flutter dev on the team, so it's difficult to discover if I'm on the wrong track.

I've ran into the same Issue. It's difficult to test wether notifyListeners was called or not especially for async functions. So I took your Idea with the listenerCallCount and put it to one function you can use.

At first you need a ChangeNotifier :

class Foo extends ChangeNotifier{
  int _i = 0;
  int get i => _i;
  Future<bool> increment2() async{
    _i++;
    notifyListeners();
    _i++;
    notifyListeners();
    return true;
  }
}

Then the function:

Future<R> expectNotifyListenerCalls<T extends ChangeNotifier, R>(
    T notifier,
    Future<R> Function() testFunction,
    Function(T) testValue,
    List<dynamic> matcherList) async {
  int i = 0;
  notifier.addListener(() {
    expect(testValue(notifier), matcherList[i]);
    i++;
  });
  final R result = await testFunction();
  expect(i, matcherList.length);
  return result;
}

Arguments:

  1. The ChangeNotifier you want to test.

  2. The function which should fire notifyListeners (just the reference to the function).

  3. A function to the state you want to test after each notifyListeners .

  4. A List of the expected values of the state you want to test after each notifyListeners (the order is important and the length has to equal the notifyListeners calls).

And this is how to test the ChangeNotifier :

test('should call notifyListeners', () async {
  Foo foo = Foo();

  expect(foo.i, 0);

  bool result = await expectNotifyListenerCalls(
      foo,
      foo.increment2,
      (Foo foo) => foo.i,
      <dynamic>[isA<int>(), 2]);

  expect(result, true);
});

Your approach seems fine to me. If you are only interested in the notifier firing (and not how often) you can use Mockito like so:

import 'package:mobile_app/example_model.dart';
import 'package:test/test.dart';

/// Mocks a callback function on which you can use verify
class MockCallbackFunction extends Mock {
  call();
}
void main() {
  group('$ExampleModel', () {
    ExampleModel exampleModel;
    final notifyListenerCallback = MockCallbackFunction(); // Your callback function mock

    setUp(() {
      exampleModel = ExampleModel()
        ..addListener(notifyListenerCallback);
      reset(notifyListenerCallback); // resets your mock
    });

    test('increments value and calls listeners', () {
      exampleModel.increment();
      expect(exampleModel.value, 1);
      exampleModel.increment();
      verify(notifyListenerCallback()); // verify listener were notified
    });

    test('unit tests are independent from each other', () {
      exampleModel.increment();
      expect(exampleModel.value, 1);
      exampleModel.increment();
      expect(notifyListenerCallback()); // verify listener were notified
    });
  });
}

I have wrap it to the function

import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

dynamic checkNotifierCalled(
  ChangeNotifier notifier,
  Function() action, [
  Matcher? matcher,
]) {
  var isFired = false;
  void setter() {
    isFired = true;
    notifier.removeListener(setter);
  }

  notifier.addListener(setter);

  final result = action();
  // if asynchronous
  if (result is Future) {
    return result.then((value) {
      if (matcher != null) {
        expect(value, matcher);
      }
      return isFired;
    });
  } else {
    if (matcher != null) {
      expect(result, matcher);
    }
    return isFired;
  }
}

and call it by:

final isCalled = checkNotifierCalled(counter, () => counter.increment(), equals(2));
expect(isCalled, isTrue);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM