Skip to main content

Command Palette

Search for a command to run...

Unit Testing in Flutter With Examples

Updated
8 min read
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.dart with 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 🐦

S

well explained ❤️. Clear and concise

More from this blog

Harish Learns Code

46 posts

Flutter SDK Engineer at Esri. I mostly write about Flutter and Rust.