Post

A deep dive into unit testing in Flutter 🧪

Testing code is an important task for a developer that helps us in ensuring that our code works as expected. Testing is an important task that should not be given the least priority or be neglected. Many companies seeking for a software developer candidate look for those who can perform testing effectively. Many open-source projects don’t prefer to merge contributions for which tests are not included. So, it is clear that testing is a must-have skill for a developer and in this blog, we will go in depth about unit testing in Flutter. 😎 Testing doesn’t help in locating bugs. It only helps in making sure that our code works as expected. It gives us the confidence that our code works even after refactoring the codebase. In Flutter, there are mainly three types of test but not limiting to. They are:

In this article, we will see what unit testing is and how we can perform unit testing with some examples.

Table of Contents

What is unit testing? 🤔

According to the official flutter documentation, unit tests are handy for verifying the behavior of a single function, method, or class. This definition is simple and cut to the point. Let’s take an example where we have a function addNumbers(int a, int b). As the name of the function suggests, the function is responsible to add two integers. Now, unit testing is written to be sure that if the two inputs are 5 and 10, then the output of the function should be 15. This is what the earlier definition meant by verifying the behavior.

Mocks and Fakes

Sometimes we might encounter a situation where our unit test depends on other classes. For example, an instance of some database, or some network client. Now, it isn’t ideal to use the real instances of such dependencies in our tests as using the real instances of such dependencies can slow down the test execution. Also, if we are dependent on some InternetChecker and if the internet service is not available then the test might not work as expected resulting into a flaky test. 💩 That’s why we use Mocks instead of real instances of dependencies. Using mocks, we can stub certain behavior so that the test runs as desired regardless of external factors.

Stubbing refers to defining certain behavior ahead of time. For example, when x is called, return y. Here, we are giving instruction that when x is called, then y is to be returned. We will see Mocks in action in the later part of this blog.

We use fakes in our test when we need to pass custom objects as parameters in our stubs or when we want to return some Fake version of any object. For example, in the statement when x is called, return y, if the signature of the method x is void x(CustomObj a), then directly passing in an instance of CustomObj as parameter in our test won’t work. In that case, we create a FakeCustomObj. If the function accepts parameters of primitive type (int, double, etc), then Fakes are not required.

In our practical implementation, we will be using a package named mocktail that helps in creating Mocks and Fakes very easily for us. 🤩

Implementation

Now, it’s time to put everything into practice. We will perform testing for a very simple code first and gradually increase the complexity. First, let’s take a look at the folder structure that we will be following.

1
2
3
4
5
6
7
8
9
10
📦root_dir       
    ├─ 📂lib       
    │  ├─ 📜file_1.dart
    │  ├─ 📜file_2.dart
    │  └─ 📜file_3.dart
    ├─ 📂test
    │  ├─ 📜file_1_test.dart
    │  ├─ 📜file_2_test.dart
    │  └─ 📜file_3_test.dart
    └─ 📜pubspec.yaml         

Most of the codes that I will be testing in this blog are taken from some open-source projects.

Test 1 (file_1.dart)

Let’s take a look at the code that we’ll be testing.

1
2
3
4
5
class Calculator {
  int add(int a, int b) {
    return a + b;
  }
}

The code is fairly simple. The method accepts two integer parameters and returns their sum.

Like I said earlier, unit testing means verifying the behavior. So, if the inputs are 2 and 3, the output has to be 5. This is what needs to be tested.

Now, inside file_1_test.dart, add the following code to test it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:test/test.dart';

void main() {
  final calculator = Calculator();

  test(
    'should return the sum of two inputs',
    () {
      final result = calculator.add(1, 2); // act

      expect(result, 3); // assert
    },
  );
}

As you can see, the testing process begins with a test function. This function takes two arguments: a string that is used to describe what this test does and a callback function inside which we write our test. The actual method which is to be tested is called from the test() function. Then, the result is matched against an expected value. If the result and the expected value (in our case 3) matches, the test passes else the test fails.

In order to run the test, go to your terminal and run

1
flutter test

It is important to add _test suffix to the file name that represents test code. Also, make sure to be on the project’s root directory in your terminal when the command flutter test is ran.

Test 2 (file_2.dart)

Let’s take a look at the code that we’ll be testing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AllowList<T> {
  final List<T> _list;

  List<T> get list => _list;

  AllowList(this._list);

  String getDescription() {
    return 'Value must of one of (${_list.join(", ")})';
  }

  String getType() {
    return T.toString();
  }

Now let’s think what are the characteristics that can be tested in this code? 🤔💭

  1. The getter list should return the actual list passed in the constructor parameter.
  2. The method getDescription() should return a string statement.
  3. The method getType() should return the data type of the elements of the list.

Now, that we know what are to be tested. Let’s see the test code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import 'package:test/test.dart';
// Add other necessary imports

void main() {
  final intList = AllowList<int>([1, 2, 3]);
  final floatingList = AllowList<double>([1.0, 2.0, 3.0]);
  final stringList = AllowList<String>(['a', 'b', 'c']);
  final nullableIntList = AllowList<int?>([1, 2, null]);

  String stringReturningFunction<T>(List<T> myList) =>
      'Value must of one of (${myList.join(", ")})';

  group(
    'allow_list_test',
    () {
      test(
        'list getter should return list value',
        () {
          expect(intList.list, [1, 2, 3]);
          expect(floatingList.list, [1.0, 2.0, 3.0]);
          expect(stringList.list, ['a', 'b', 'c']);
          expect(nullableIntList.list, [1, 2, null]);
        },
      );

      test(
        'getDescription: should return proper description',
        () {
          expect(
            intList.getDescription(),
            stringReturningFunction<int>([1, 2, 3]),
          );
          expect(
            floatingList.getDescription(),
            stringReturningFunction<double>([1.0, 2.0, 3.0]),
          );
          expect(
            stringList.getDescription(),
            stringReturningFunction<String>(['a', 'b', 'c']),
          );
          expect(
            nullableIntList.getDescription(),
            stringReturningFunction<int?>([1, 2, null]),
          );
        },
      );

      test(
        'getType(): should return proper data type',
        () {
          expect(intList.getType(), 'int');
          expect(floatingList.getType(), 'double');
          expect(stringList.getType(), 'String');
          expect(nullableIntList.getType(), 'int?');
        },
      );
    },
  );
}

Now, run the test using the command flutter test and the test should pass. I used a new term here named group. A group is basically where you’d place relevant tests together.

Notice that I have created four different instances of AllowList class in the above test. The test would have passed with just one to be honest. But the fact that AllowList accepts generic constructor parameter, it becomes our duty as a developer to test against different cases.

Test 3 (file_3.dart)

Now, let’s try testing a more difficult code. This is going to be the last code we’ll be testing in this blog. The code we will be testing looks like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
typedef InnerClient = http.Client;

class KhaltiHttpClient extends KhaltiClient {
  KhaltiHttpClient({
    InnerClient? client,
  }) : _client = client ?? InnerClient();

  final InnerClient _client;

  @override
  Future<HttpResponse> get(
    String url,
    Map<String, Object> params,
  ) async {
    return _handleExceptions(
      () async {
        final uri = Uri.parse(url).replace(queryParameters: params);
        final response = await _client.get(
          uri,
          headers: KhaltiService.config.raw,
        );
        final statusCode = response.statusCode;
        final responseData = jsonDecode(response.body);

        if (_isStatusValid(statusCode)) {
          return HttpResponse.success(
            data: responseData,
            statusCode: statusCode,
          );
        }
        return HttpResponse.failure(
          data: responseData,
          statusCode: statusCode,
        );
      },
    );
  }

  @override
  Future<HttpResponse> post(
    String url,
    Map<String, Object?> data,
  ) {
    return _handleExceptions(
      () async {
        final uri = Uri.parse(url);
        final response = await _client.post(
          uri,
          body: data,
          headers: KhaltiService.config.raw,
        );
        final statusCode = response.statusCode;
        final responseData = jsonDecode(response.body);

        if (_isStatusValid(statusCode)) {
          return HttpResponse.success(
            data: responseData,
            statusCode: statusCode,
          );
        }
        return HttpResponse.failure(
          data: responseData,
          statusCode: statusCode,
        );
      },
    );
  }

  bool _isStatusValid(int statusCode) => statusCode >= 200 && statusCode < 300;

  Future<HttpResponse> _handleExceptions(
    Future<HttpResponse> Function() caller,
  ) async {
    try {
      return await caller();
    } on HttpException catch (e, s) {
      return HttpResponse.exception(
        message: e.message,
        code: 0,
        stackTrace: s,
        detail: e.uri,
      );
    } on http.ClientException catch (e, s) {
      return HttpResponse.exception(
        message: e.message,
        code: 0,
        stackTrace: s,
        detail: e.uri,
      );
    } on SocketException catch (e, s) {
      return HttpResponse.exception(
        message: e.message,
        code: e.osError?.errorCode ?? 0,
        stackTrace: s,
        detail: e.osError?.message,
        isSocketException: true,
      );
    } on FormatException catch (e, s) {
      return HttpResponse.exception(
        message: e.message,
        code: 0,
        stackTrace: s,
        detail: e.source,
      );
    } catch (e, s) {
      return HttpResponse.exception(
        message: e.toString(),
        code: 0,
        stackTrace: s,
      );
    }
  }
}

Let’s first understand what this code does in detail before discussing about its test.

We have a class KhaltiHttpClient that depends on http.Client. http.Client’s instance is necessary to make network requests internally. We have two public methods: get() and post(). Both methods get() and post() make some network calls. This operation is wrapped within a try-catch block inside _handleExceptions() method.

Now, if we were to test this class, what are the different scenarios we will have to test? 🤔💭 Let’s list that out one by one.

  • An instance of InnerClient should be instantiated when nothing is passed as a parameter in the KhaltiHttpClient constructor.
  • The get() method:
    • should return HttpResponse.success() if the status code is >= 200 and < 300.
    • should return HttpResponse.failure() if the status code is >= 400 and <600.
    • should return HttpResponse.exception() if HttpException is thrown.
    • should return HttpResponse.exception() if ClientException is thrown.
    • should return HttpResponse.exception() if SocketException is thrown.
    • should return HttpResponse.exception() if FormatException is thrown.
    • should return HttpResponse.exception() if any Exception is thrown.
  • The post() method:
    • should return HttpResponse.success() if the status code is >= 200 and < 300.
    • should return HttpResponse.failure() if the status code is >= 400 and <600.
    • should return HttpResponse.exception() if HttpException is thrown.
    • should return HttpResponse.exception() if ClientException is thrown.
    • should return HttpResponse.exception() if SocketException is thrown.
    • should return HttpResponse.exception() if FormatException is thrown.
    • should return HttpResponse.exception() if any Exception is thrown.

It might seem like a lot of work and troublesome but this is what unit testing is all about. You don’t want to miss a single scenario from being tested.

If you are thinking what HttpResponse class is, then this is what this class looks like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/// The response for [KhaltiClient].
class HttpResponse {
  const HttpResponse._({this.data, this.statusCode, this.message});

  /// The [data] received.
  final Object? data;

  /// The [statusCode] of response.
  final int? statusCode;

  /// The error [message].
  final String? message;

  /// Factory for [SuccessHttpResponse].
  factory HttpResponse.success({
    required Object data,
    required int statusCode,
  }) = SuccessHttpResponse._;

  /// Factory for [FailureHttpResponse].
  factory HttpResponse.failure({
    required Object data,
    required int statusCode,
  }) = FailureHttpResponse._;

  /// Factory for [ExceptionHttpResponse].
  factory HttpResponse.exception({
    required String message,
    required int code,
    required StackTrace stackTrace,
    Object? detail,
    bool isSocketException,
  }) = ExceptionHttpResponse._;
}

/// The success response for [KhaltiClient].
class SuccessHttpResponse extends HttpResponse {
  const SuccessHttpResponse._({
    required Object data,
    required int statusCode,
  }) : super._(data: data, statusCode: statusCode);

  @override
  String toString() {
    return 'SuccessHttpResponse{data: $data, statusCode: $statusCode}';
  }
}

/// The failure response for [KhaltiClient].
class FailureHttpResponse extends HttpResponse {
  const FailureHttpResponse._({
    required Object data,
    required int statusCode,
  }) : super._(data: data, statusCode: statusCode);

  @override
  String toString() {
    return 'FailureHttpResponse{data: $data, statusCode: $statusCode}';
  }
}

/// The exception for [KhaltiClient].
class ExceptionHttpResponse extends HttpResponse {
  /// The error [code].
  final int code;

  /// The [stackTrace] of the exception.
  final StackTrace stackTrace;

  /// The exception detail
  final Object? detail;

  /// Defines whether the exception is socket exception or not.
  final bool isSocketException;

  const ExceptionHttpResponse._({
    required String message,
    required this.code,
    required this.stackTrace,
    this.detail,
    this.isSocketException = false,
  }) : super._(message: message, statusCode: code);

  @override
  String toString() {
    return 'ExceptionHttpResponse{message: $message, code: $code, stackTrace: $stackTrace, detail: $detail, isSocketException: $isSocketException}';
  }
}

And this is its unit test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void main() {
  group('HttpResponse tests | ', () {
    test('success factory', () {
      final response = HttpResponse.success(
        data: 'data',
        statusCode: 200,
      );

      expect(response, isA<SuccessHttpResponse>());
      expect(
        response.toString(),
        'SuccessHttpResponse{data: data, statusCode: 200}',
      );
    });

    test('failure factory', () {
      final response = HttpResponse.failure(
        data: 'Unauthorized',
        statusCode: 403,
      );

      expect(response, isA<FailureHttpResponse>());
      expect(
        response.toString(),
        'FailureHttpResponse{data: Unauthorized, statusCode: 403}',
      );
    });

    test('exception factory', () {
      final response = HttpResponse.exception(
        message: 'No connection',
        code: 7,
        stackTrace: StackTrace.empty,
        detail: 'Could not reach the server',
        isSocketException: true,
      );

      expect(response, isA<ExceptionHttpResponse>());
      expect(
        response.toString(),
        'ExceptionHttpResponse{message: No connection, code: 7, stackTrace: , detail: Could not reach the server, isSocketException: true}',
      );
    });
  });
}

Since, the test for HttpResponse is pretty simple, I won’t be explaining it here.

Now, let’s begin writing our test for KhaltiHttpClient.

  • should instantiate internal http.Client when not injected via KhaltiHttpClient constructor.
1
2
3
4
5
6
7
8
9
10
11
group(
    'KhaltiHttpClient constructor',
    () {
      test(
        'instantiates internal httpClient when not injected',
        () {
          expect(KhaltiHttpClient(), isNotNull);
        },
      );
    },
  );

This is all we need to do to test this behavior as initializing KhaltiHttpClient() will 101% initialize the internal http.Client. If you run this test using the command flutter test, you will see that the test will pass.

Now, let’s test for the get() method which is a bit tricky as few things that we haven’t implemented yet are involved such as Mocks and Fakes.

If we see the get() method in the KhaltiHttpClient, the method internally uses http.Client’s instance to make network requests. The line

1
2
3
4
final response = await _client.get(
  uri,
  headers: KhaltiService.config.raw,
);

is what makes the request to the server. This line is also prone to several exceptions so we need to write our test in such a way that this method doesn’t throw any exceptions. But how can we determine how an external package works? The thing is we can’t.

We will be testing for a scenario that the get() method should return HttpResponse.success() if the status code is >= 200 and < 300. And in order to return HttpResponse.success(), the _client.get() has to return proper data.

Since, we can’t rely on the real class’ instance for the reasons I had stated above, we create a mocked version of http.Client. And then we will call _client.get() from out mocked instance of http.Client.

We need to add a package mocktail to our dev_dependencies inside pubspec.yaml file.

The way we create our Mock is by a simple one-liner.

1
class _MockClient extends Mock implements http.Client {}

That’s it. Now, we can use _MockClient as regular http.Client such as

1
2
3
final mockClient = _MockClient();
// We can now do 
// mockClient.get(...) or mockClient.post(...)

mockClient.get() will return an instance of http.Response() which is again part of the http library.

So, we create a mocked version of http.Response too.

1
class _MockResponse extends Mock implements http.Response {}

The mockClient.get() takes in two arguments as its parameter: an Uri object and a Map.

Because an Uri object isn’t dart primitive type and is used as a parameter, we will need to create a _FakeUri so that it can be used.

1
class _FakeUri extends Fake implements Uri {}

And inside the main function in the test, add this line.

1
2
3
 setUpAll(() {
    registerFallbackValue(_FakeUri());
  });

Now, we are ready to write our test. We will write our test in AAA format which stands for Arrange, Act, Assert.

Arrange is where we stub certain behavior. Act is where we call our methods that we need to test. Assert is where we perform out expect or verify block.

Because, we are expecting our get() to return HttpResponse.success(), our mockClient.get() should return a response with a status code of 200 (which refers to successful network request) and this is something we explicitly need to mention (the arrange phase aka stubbing).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const _testUrl = 'www.url.com';
const _testParams = {'key': 'value'};
const _testResponseBody = '{"message": "Fake Response"}';

void main() {
  final mockClient = _MockClient();
  final khaltiHttpClient = KhaltiHttpClient(client: client);

  test(
    'should return HttpResponse.success() if the status code is >= 200 and < 300',
    () {
      final response = _MockResponse();

      // arrange
      when(() => response.body).thenReturn(_testResponseBody);
      when(() => response.statusCode).thenReturn(200);
      when(
        () => client.get(
          any(),
          headers: any(named: 'headers'),
        ),
      ).thenAnswer((_) async => response);

      // act
      final result = khaltiHttpClient.get(_testUrl, _testParams);

      //assert
      expect(
        result,
        HttpResponse(
          data: jsonDecode(_testResponseBody),
          statusCode: 200,
        ),
      );
    },
  );
}

Everything looks fine but if we try running this test, the test will fail and the reason is the way we wrote our expect block.

Inside the expect block, we are comparing two different Dart objects. And because Dart works on referential equality i.e. if we are to compare equality of two objects in Dart, then the memory addresses of the two objects are checked. If the memory addresses for both the objects are same, then the objects are equal else unequal.

So, how can we enforce value equality in Dart? There are few ways. One of them would be to use equatable package and extend our HttpRespone class with Equatable. But this is not always the best idea. What if the HttpResponse API comes from a third party package? We can’t just change its code if it’s coming from a third party package.

Another way would be to manually override hashCode and ==. But again this can’t be considered if HttpResponse API comes from a third party package.

So, we take the help of TypeMatcher that comes from the test package.

So, in the above test, update the expect statement to this. 👇️

1
2
3
4
5
6
expect(
  result,
  isA<HttpResponse>()
      .having((e) => e.data, 'data', jsonDecode(_testResponseBody))
      .having((e) => e.statusCode, 'status code', statusCode),
);

So, the test would be something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void main() {
  final mockClient = _MockClient();
  final khaltiHttpClient = KhaltiHttpClient(client: client);

  test(
    'should return HttpResponse.success() if the status code is >= 200 and < 300',
    () {
      final response = _MockResponse();

      // arrange
      when(() => response.body).thenReturn(_testResponseBody);
      when(() => response.statusCode).thenReturn(200);
      when(
        () => client.get(
          any(),
          headers: any(named: 'headers'),
        ),
      ).thenAnswer((_) async => response);

      // act
      final result = khaltiHttpClient.get(_testUrl, _testParams);

      //assert
      expect(
        result,
        isA<HttpResponse>()
            .having((e) => e.data, 'data', jsonDecode(_testResponseBody))
            .having((e) => e.statusCode, 'status code', statusCode),
      );
    },
  );
}

And now if you run the test using the command flutter test, the test would pass.

Similarly, if entire code is tested, we will have a test code that looks something like this after some refactoring.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:khalti/khalti.dart';
import 'package:mocktail/mocktail.dart';

typedef _MethodUnderTestCaller = Future<HttpResponse> Function(
  String,
  Map<String, Object>,
);

const _testUrl = 'www.url.com';
const _testParams = {'key': 'value'};
const _testResponseBody = '{"message": "Fake Response"}';

void _runSuccessOrFailureTest({
  required int statusCode,
  required Future<http.Response> Function() stubMethod,
  required _MethodUnderTestCaller caller,
}) async {
  final response = _MockResponse();

  when(() => response.statusCode).thenReturn(statusCode);
  when(() => response.body).thenReturn(_testResponseBody);
  when(stubMethod).thenAnswer((_) async => response);

  final result = await caller(_testUrl, _testParams);

  expect(
    result,
    isA<HttpResponse>()
        .having((e) => e.data, 'data', jsonDecode(_testResponseBody))
        .having((e) => e.statusCode, 'status code', statusCode),
  );
}

void _runExceptionTest({
  required Exception exception,
  required Future<http.Response> Function() stubMethod,
  required _MethodUnderTestCaller caller,
}) async {
  when(stubMethod).thenThrow(exception);

  final result = await caller(_testUrl, _testParams);

  expect(
    result,
    isA<HttpResponse>()
        .having((e) => e.data, 'data', isNull)
        .having((e) => e.statusCode, 'status code', 0)
        .having((e) => e.message, 'exception', 'Exception'),
  );
}

void main() {
  final mockClient = _MockClient();
  final khaltiHttpClient = KhaltiHttpClient(client: mockClient);

  setUpAll(() {
    registerFallbackValue(_FakeUri());
  });

  group(
    'KhaltiHttpClient constructor',
    () {
      test(
        'instantiates internal httpClient when not injected',
        () {
          expect(KhaltiHttpClient(), isNotNull);
        },
      );
    },
  );

  group(
    'KhaltiHttpClient |',
    () {
      group(
        'get():',
        () {
          test(
            'should return HttpResponse.success() if the status code is >= 200 and < 300',
            () {
              _runSuccessOrFailureTest(
                statusCode: 200,
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );

          test(
            'should return HttpResponse.failure() if the status code is >= 400 and <600',
            () {
              _runSuccessOrFailureTest(
                statusCode: 404,
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if HttpException is thrown',
            () {
              _runExceptionTest(
                exception: const HttpException('Exception'),
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if ClientException is thrown',
            () {
              _runExceptionTest(
                exception: http.ClientException('Exception'),
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if SocketException is thrown',
            () {
              _runExceptionTest(
                exception: const SocketException('Exception'),
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if FormatException is thrown',
            () {
              _runExceptionTest(
                exception: const FormatException('Exception'),
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if any Exception is thrown',
            () {
              _runExceptionTest(
                exception: Exception(),
                stubMethod: () => mockClient.get(
                  any(),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.get,
              );
            },
          );
        },
      );

      group(
        'post():',
        () {
          test(
            'should return HttpResponse.success() if the status code is >= 200 and < 300',
            () {
              _runSuccessOrFailureTest(
                statusCode: 200,
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );

          test(
            'should return HttpResponse.failure() if the status code is >= 400 and <600',
            () {
              _runSuccessOrFailureTest(
                statusCode: 404,
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if HttpException is thrown',
            () {
              _runExceptionTest(
                exception: const HttpException('Exception'),
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if ClientException is thrown',
            () {
              _runExceptionTest(
                exception: http.ClientException('Exception'),
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if SocketException is thrown',
            () {
              _runExceptionTest(
                exception: const SocketException('Exception'),
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if FormatException is thrown',
            () {
              _runExceptionTest(
                exception: const FormatException('Exception'),
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );

          test(
            'should return HttpResponse.exception() if any Exception is thrown',
            () {
              _runExceptionTest(
                exception: Exception(),
                stubMethod: () => mockClient.post(
                  any(),
                  body: any(named: 'body'),
                  headers: any(named: 'headers'),
                ),
                caller: khaltiHttpClient.post,
              );
            },
          );
        },
      );
    },
  );
}

class _MockClient extends Mock implements http.Client {}

class _MockResponse extends Mock implements http.Response {}

class _FakeUri extends Fake implements Uri {}

Checking Code Coverage

After writing the test and successfully running it, you might want to check if you tested every part of your code. The way you can find that is by checking the code-coverage. In order to do so in flutter, follow the following steps.

  • First, run the command flutter test --coverage (Make sure that you are in the root project directory in your terminal).
  • After running the above command, a new directory by the name coverage would appear in the root directory which contains a single file lcov.info.
  • Now, copy the command shown below, paste it in the terminal and run it.
1
genhtml ./coverage/lcov.info -o coverage

Make sure to install lcov in your system to run genhtml command.

  • If everything is fine, then bunch of files will be created inside the coverage directory. It will have a file named index.html. Open the file in your browser and you should see a web page opened that looks something like this. Screenshot displaying code coverage in the browser You can see more information about what lines are untested (if there are any) by opening the file by clicking on the file name.

Alternatively, if you are using VS Code, you can also install extensions like coverage gutters or flutter coverage that helps in viewing code coverage right inside VS Code.

Conclusion

So, that was it. In this blog, we learned about unit testing, and about what, why and how of unit testing. Unit testing is a fun task to do as you get used to with it.

You can always check existing open-source projects to learn more about unit tests and to see how others do it. If you enjoyed reading this blog and found that it helped to hone your skill as a developer, then please share the blog among your developer friends. If there is any feedback you’d want to leave, please do so in the comments section below.

If you wish to see some Flutter projects with proper architecture, follow me on GitHub. I am also active on Twitter @b_plab where I tweet about Flutter and Android.

My Socials:

Until next time, happy coding!!! 👨‍💻

— Biplab Dutta

Credit

raywenderlich.com for the preview image.

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.