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.
Once the app is complete, you will write the following tests: Unit tests to validate the add and remove operations. Widgets tests for the home and favorites pages. UI and performance tests for the entire app using integration tests.
In general, test files should reside inside a test folder located at the root of your Flutter application or package. Unit and Widget tests must be located in a test folder. Integration test must go in a separate directory called test_driver . Both folders must be located same level as your lib folder.
Your approach seems fine to me but if you want to have a more descriptive way you could also use Mockito to register a mock callback function and test whether and how often the notifier is firing and thus notifying your registered mock instead of incrementing a counter:
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', () {
late ExampleModel exampleModel;
final notifyListenerCallback = MockCallbackFunction(); // Your callback function mock
setUp(() {
exampleModel = ExampleModel()
..addListener(notifyListenerCallback);
reset(notifyListenerCallback); // resets your mock before each test
});
test('increments value and calls listeners', () {
exampleModel.increment();
expect(exampleModel.value, 1);
exampleModel.increment();
verify(notifyListenerCallback()).called(2); // verify listener were notified twice
});
test('unit tests are independent from each other', () {
exampleModel.increment();
expect(exampleModel.value, 1);
exampleModel.increment();
expect(notifyListenerCallback()).called(2); // verify listener were notified twice. This only works, if you have reset your mocks
});
});
}
Just keep in mind that if you trigger the same mock callback function in multiple tests you have to reset your mock callback function in the setup to reset its counter.
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:
The ChangeNotifier
you want to test.
The function which should fire notifyListeners
(just the reference to the function).
A function to the state you want to test after each notifyListeners
.
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);
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With