Unit Test the Change Notifier in Flutter
This is the Third article in the “Master Testing In Flutter” series. The Previous article was about Unit Testing the Http function to get all the quotes from the API. If you have not read the previous blogs, I would advise you to read them before you continue with the Unit Test the Change Notifier. Here is the list of other articles in this series, in case you want to read it.
- Introduction to Testing and Types of Testing in Flutter
- Unit Test
- Unit Test the Change Notifier📍
- Widget and Integration Test
- Generate Goldens
- Code Coverage
Let’s start with Unit Testing the Change Notifier. As we previously discussed in Unite Test we test small functions or classes. Before starting with testing let us first explore the todo_notifier
. Navigate to todo_notifier.dart
class TodoNotifier extends ChangeNotifier {
final TodoService _todoService;
TodoNotifier(this._todoService);
List<Todo> _todos = [];
List<Todo> get todos => _todos;
bool _isLoading = false;
bool get isLoading => _isLoading;
void setLoader(bool value) {
_isLoading = value;
notifyListeners();
}
Future getTodos() async {
setLoader(true);
_todos = await _todoService.getTodos();
setLoader(false);
}
}
final _client = Client();
final todosNotifierProvider =
ChangeNotifierProvider((ref) => TodoNotifier(TodoService(_client)));
- This code defines a
TodoNotifier
class that extendsChangeNotifier
, a class provided by the Flutter framework that can notify its listeners when its state changes. - The
TodoNotifier
class has a_todoService
property that represents an instance of aTodoService
class, which communicates with a backend API to fetch todo items. - The
_todos
property is a list ofTodo
objects, and thegetTodos
method fetches the todos from the API by calling_todoService.getTodos()
. ThesetLoader
method is used to toggle the_isLoading
boolean property, which represents whether the todos are currently being fetched or not. - When the
setLoader
method is called, it sets the_isLoading
property to the passed value and notifies its listeners of the state change by callingnotifyListeners()
. - The
isLoading
property returns the current value of_isLoading
. Thetodos
property returns the current value of_todos
. - Finally, the code defines a
todosNotifierProvider
variable that creates aChangeNotifierProvider
instance. TheChangeNotifierProvider
is a widget that provides theTodoNotifier
instance to its child widgets via theProvider
package. TheTodoNotifier
instance is constructed with an instance ofTodoService
, which is constructed with aClient
instance.
Test Change Notifier
Create a new file todo_change_notifier_test.dart
in a test folder. This is where we will write the tests to test the change notifier. Add the following test code in the main
function.
void main() {
late TodoNotifier sut_todoNotifier;
setUp(() {
sut_todoNotifier = TodoNotifier();
});
}
The above code declares a variable named sut_todoNotifier
of type TodoNotifier
as late
, which means that it is uninitialized but will be assigned a value before it is used.
The prefix sut
is commonly used as a naming convention in software testing to indicate that this is a System Under Test (SUT) object, which is the object being tested.
The setUp()
function is used to set up the test environment before running each test case. In this case, it initializes the sut_todoNotifier
variable by creating a new instance of the TodoNotifier
class.
sut_todoNotifier = TodoNotifier()
throws an error as it requires TodoService
at the time of initialisation.
We have to use a mock object like MockTodoService
which is useful in testing scenarios where the real TodoService
object is difficult to use, has unpredictable behaviour, or is unavailable. By using a mock object, developers can isolate the behaviour of the TodoService
and test the behaviour of other parts of the system without worrying about the state of the TodoService
itself.
Mocking Data with Mocktail
Add the following code on top of
class MockTodoService extends Mock implements TodoService {}
Note: — You will need to add
mocktail
package in thepubspec.yaml
This code defines a mock class called MockTodoService
that extends the Mock
class and implements the TodoService
interface. The Mock
class is part of a testing mocktail that allows developers to create mock objects that simulate the behaviour of real objects for testing purposes.
By extending Mock
, the MockTodoService
class inherits methods that can be used to set up expectations and define behaviour for the mock object. These expectations and behaviours can be used to test how other parts of the system interact with the TodoService
object.
Pass the MockTodoService
variable inside the constructor of sut_todoNotifier();
late TodoNotifier sut_todoNotifier;
late MockTodoService mockTodoService;
setUp(() {
mockTodoService = MockTodoService();
sut_todoNotifier = TodoNotifier(mockTodoService);
});
Initial Testing
First, we have to test the Initial values when the change notifier initialises
test('Should check initial values are correct', () {
expect(sut_todoNotifier.isLoading, false);
expect(sut_todoNotifier.todos, []);
});
The first assertion checks that the isLoading
property is initially set to false
. This means that when the TodoNotifier
object is created, it should not be in a loading state.
The second assertion checks that the todos
property is initially set to an empty list. This means that when the TodoNotifier
object is created, it should not contain any todos.
void getTodoServiceReturnsQuotes() {
when(() => mockTodoService.getTodos())
.thenAnswer((_) async => mockTodosForTesting);
}
This code is defining a mock behaviour for a TodoService
instance using the mockTodoService
object. Specifically, it is defining the behaviour of the getTodos()
method of the TodoService
.
The when()
function is a part of the mocktail library, and it is used to define the behaviour of a mocked object when a specific method is called on it. In this case, when()
is called with an anonymous function that returns the result of calling getTodos()
on the mockTodoService
object.
The thenAnswer()
method is called on the result of the when()
function, and it defines the behaviour that should be returned when the method is called. In this case, it is returning a Future
that resolves to mockTodosForTesting
, which is a predefined list of Todo
objects that are being used for testing purposes. It is located in key_constants.dart
file
Overall, this code is defining a mock behaviour for a TodoService
instance that always returns a predefined list of Todo
objects when its getTodos()
method is called. This is useful for testing code that relies on the behaviour of the TodoService
without actually having to interact with a real instance of the service.
Testing the getTodo function
Now let’s test the getTodo function from our Change Notifier. Add the following test code.
test('''Loading data indicator,
sets Todos, indicates data is not loaded anymore''', () async {
getTodoServiceReturnsQuotes();
final future = sut_todoNotifier.getTodos();
expect(sut_todoNotifier.isLoading, true);
await future;
expect(sut_todoNotifier.todos, mockTodosForTesting);
expect(sut_todoNotifier.isLoading, false);
});
The test uses the getTodoServiceReturnsQuotes()
function to simulate the loading of Todos, which likely retrieves the Todos from a service or data source. Then, the test calls the getTodos()
method and verifies that the loading state of the sut_todoNotifier
instance is set to true
.
After that, the test awaits the completion of the future
variable, which should represent the loading of the Todos. Once the loading is complete, the test verifies that the Todos have been correctly loaded by checking that the todos
property of sut_todoNotifier
contains the expected values.
Finally, the test checks that the loading state of the sut_todoNotifier
instance is set to false
after the Todos have been loaded.
Grouping All The Tests
Now we have successfully written all the tests for our change notifier, you can run each test and check. Also, you will notice that we have to run each test separately which is a tedious task.
Let’s group all the tests so that we can run them all together.
group('Group of Tests for testing change notifier', () {});
Just as we have test
function to write the tests, we have a group
function, which groups all the tests written. Just add the written tests inside the callback function of the group test.
Finally todo_change_notifier_test.dart
should look like this.
class MockTodoService extends Mock implements TodoService {}
void main() {
late TodoNotifier sut_todoNotifier;
late MockTodoService mockTodoService;
setUp(() {
mockTodoService = MockTodoService();
sut_todoNotifier = TodoNotifier(mockTodoService);
});
group('Group of Tests for testing change notifier', () {
test('Should check initial values are correct', () {
expect(sut_todoNotifier.isLoading, false);
expect(sut_todoNotifier.todos, []);
});
void getTodoServiceReturnsQuotes() {
when(() => mockTodoService.getTodos())
.thenAnswer((_) async => mockTodosForTesting);
}
test('''Loading data indicator,
sets Todos, indicates data is not loaded anymore''', () async {
getTodoServiceReturnsQuotes();
final future = sut_todoNotifier.getTodos();
expect(sut_todoNotifier.isLoading, true);
await future;
expect(sut_todoNotifier.todos, mockTodosForTesting);
expect(sut_todoNotifier.isLoading, false);
});
});
}
Here I conclude the third article from Master Testing in Flutter series. In this article, I explained how you can Test the Change Notifier and mock data with the Mocktail package.
This also concludes the Unit Testing part of our Testing Series. In the next article. I will cover Widget Testing, and concepts of testing a widget.
If you want to check out the rest of the blogs from the testing Series here is a list of all the blogs present. I would still address if you follow this guide step by step.
- Introduction to Testing and Types of Testing in Flutter
- Unit Test
- Unit Test the Change Notifier📍
- Widget and Integration Test
- Generate Goldens
- Code Coverage
Thank You for reading my blog, if you have any doubts or recommendations please let me know in the comment section below. I would like to know what are the other way to globally listen to network connectivity.
You have 50 Claps per day. Claps do Motivate us to keep writing. Gotta Use Them All