Unit Testing in Flutter With Examples

You can find the source code for the examples here: Github Link
What is a Unit Test?
A unit test is a test that verifies the behavior of an isolated piece of code. It can be used to test a single function, method or class.
Remember: "unit" is another name for a function, method or a class
Let's start with the basic Counter app we get as the default app in Flutter.
Example 1: Basic Counter App:
Actual Code:
This is the default app we get with flutter.
Initially separate the counter code from the UI code. So we just put it in a file named counter.dart:
class Counter {
int _counter = 0;
int get count => _counter;
void incrementCount() {
_counter += 1;
}
}
Testing:
Now we can write a unit test for this in a file named
counter_test.dart.As for the name convention it always must end in
_test.dartwith the start being any name.In order to test. We'll always have to import
'package:flutter_test/flutter_test.dart';
Tests are defined using the top-level test functions and you can check if the results are correct using the top-level expect function. So we can call a test(description,body) function inside our main().
test(description, body):
Description: The standard convention when describing a test class is:
GIVEN WHEN THEN
For example in the current scenario we want to check if the count value is 0 when the counter class is instantiated.
So we'll write: Given counter class when it is instantiated then the value of the count should be 0.
Body: In here we are going to do three things: ARRANGE ACT ASSERT
ARRANGE: We will declare an object of the class
ACT: We will get the value of the count
ASSERT: We will check if the value is 0
Testing Code:
Count Test:
And here's the final counter_test.dart:
import 'package:basic_counter/counter.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// given when then
test('Given counter class when it is instantiated then value of the count should be 0', (){
// Arrange
final Counter counter = Counter();
// Act
final val = counter.count;
// Assert
expect(val, 0);
});
}
We can test the code using the following command:
flutter test
incrementCount() test
We can also test the incrementCount(): using the following code:
// given when then
test('Given counter class when incrementCount() is called then the value of the count should be 1',(){
// Arrange
final Counter counter = Counter();
// Act
counter.incrementCount();
final val = counter.count;
// Assert
expect(val, 1);
});
}
Grouping Tests
Since both of the tests are for the Counter class. we can group them together like this:
// Given When Then
group('Test start, increment', () {
// Arrange
final Counter counter = Counter();
test(
'Given counter class when it is instantiated then value of the count should be 0',
() {
// Act
final val = counter.count;
// Assert
expect(val, 0);
});
// given when then
test(
'Given counter class when incrementCount() is called then the value of the count should be 1',
() {
// Act
counter.incrementCount();
final val = counter.count;
// Assert
expect(val, 1);
});
});
To run all the tests you put in a group, run the following command:
flutter test --plain-name "Test start, increment"
Pre-test and Post-test:
Imagine if we added another function to Counter called decrementCounter():
void decrementCounter() {
_counter--;
}
Now we'll have to again add another test:
test(
'Given counter class when decrementCount() is called then the value of the count should be -1',
() {
counter.decrementCount();
final val = counter.count;
expect(val, -1);
});
The problem is that the above test will fail because in the incrementCount test we've increased the value to 1. So now val will be 0. This is why Flutter provides us with pre-test and post-test functions.
Pre-test Functions:
There are two pretest functions:
setUp()setUpAll()
setUp():
This function will run before every test. So let's say we have 3 tests:
Then it'll be setUp --> test --> setUp --> test --> setUp --> test
So in order to solve the above problem of running multiple tests with the same instance of the Counter() we can declare it like this:
void main() {
late Counter counter;
setUp((){
counter = Counter();
});
test()...
....
}
setUpAll():
This function will run only once.
So it'll be: setUpAll --> test --> test --> test
Post-test Functions:
There are two post-test functions:
tearDown()tearDownAll()
tearDown():
The function will run after every test.
test -> tearDown -> test -> tearDown
tearDownAll():
The function will only run once after all the tests are done.
test -> test -> test -> tearDownAll
And thanks to using the pre-test functions. We are able to run all the tests successfully. You can find the source code for the project here: Github Link
Example 2: HTTP User App
In this app we are goin to test some fundamental network calls using http from pub.dev.
We are going to call a user from JsonPlaceHolder site.
Imagine we have define a basic User Model:
class User {
int id;
String name;
String username;
String email;
String phone;
}
getUser():
Now we can define a UserRepository class where we can get the user from network from jsonPlaceholder:
class UserRepository {
Future<User> getUser() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
);
if (response.statusCode == 200) {
final responseBody = jsonDecode(response.body);
return User.fromJson(responseBody);
}
throw Exception('Some Error Occurred');
}
}
Testing:
We have multiple testing scenarios
Test if it returns an object of the type isUser
void main() {
late UserRepository userRepository;
// Pre-test function
setUp(() {
userRepository = UserRepository();
});
group('User Repository -', () {
group('getUser Function -', () {
test(
'given UserRepository class when getUser function is called and status code is 200 then the returned object should be a User Model',
() async {
// Act
final user = await userRepository.getUser();
// Assert
expect(user, isA<User>());
});
});
});
}
If you look at the expect() The way we check if it is an object is by using isA<User>() function.
Test if it returns an Exception when the status code is not 200
test(
'given userRepository class when getUser function is called and the status code is not 200 then the function throws an exception',
() async {
// Act
final user = await userRepository.getUser();
// Assert
expect(user, throwsException);
});
We can check for an exception using throwsException
But the problems is when we run it. The test it is obviously going to fail because the status code is going to be 200. So how do we test the exception test block ?
We are going to use external packages for testing. Some of the best ones are mockito and mocktail.
In this scenario we are going to use mocktail.
Dependency Injection for Testing:
If we take a look at our getUser() function. We see that it is dependent on http package.
So whenever there is an external dependency we need to get control over them so that we can test them out properly. This is basically dependency injection.
Since our main dependency is http. Let's put in the constructor so that we can inject it from anywhere so we are going to change the UserRepository to this
class UserRepository {
final http.Client client;
UserRepository(this.client);
Future<User> getUser() async {
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
);
if (response.statusCode == 200) {
final responseBody = jsonDecode(response.body);
return User.fromJson(responseBody);
}
throw Exception('Some Error Occurred');
}
}
Since we are calling a constructor, we can switch the http client to a mock client if we want for testing purposes. A mock client allows us to modify its behavior.
For now we will be using the Mocktail from pub.dev as we can pass in a mock client.
We are going to add Mocktail to the dev dependency
flutter pub add -d mocktail
Why do we need external Mock packages?
Imagine if we wanted to create a MockHTTPClient of Client from HTTP. We get the following:

The problem is that we'll have to implement 10 missing overrides just to create the client which obviously is an extremely heavy task.
This is where Mocktail comes into play. So we can write the following code:
class MockHTTPClient extends Mock implements Client {}
and it doesn't cause any errors.
Using Mocktail inside the test function.
We have to use the when() to declare the expected output for the mockHTTPClient
StatusCode 200 test:
test(
'given UserRepository class when getUser function is called and status code is 200 then the returned object should be a User Model',
() async {
// Arrange
when(() => mockHTTPClient
.get(Uri.parse('https://jsonplaceholder.typicode.com/users/1')))
.thenAnswer((invocation) async {
return Response('''
{ "id": 1, "name": "Leanne Graham", "username": "Bret", "email": "Sincere@april.biz", "phone": "1-770-736-8031 x56442", "website": "hildegard.org"}
''', 200);
});
// Act
final user = await userRepository.getUser();
// Assert
expect(user, isA<User>());
});
Throw Exception test:
test(
'given userRepository class when getUser function is called and the status code is not 200 then the function throws an exception',
() async {
// Arrange
when(
() => mockHTTPClient.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
),
).thenAnswer(
(invocation) async => Response('', 500),
);
// Act
final user = userRepository.getUser();
// Assert
expect(user, throwsException);
});
});
As you can see here i just changed the status code and this resulted in a exception which satisfies our test case.
And this basically covers Unit Testing basics. Also these are just my notes after watching the excellent video from Rivan Ranawat. You can find the video here: Youtube Link
Happy Coding 🐦



