Flutter Unit Testing Part 1

Hedighodhbane
7 min readNov 20, 2022

--

let’s start with the What.

Testing is just about writing some logic to tell you whether or not the result you would expect from your code is valid or not.

You might for example expect to have a blue button when you start the app, or you might expect the value to increment by one when you click on a button.

But of course here we’re talking about automated testing, so it might be some code that will do this for you and give you the result after interacting with your code.

An important note here is that Unit Testing or any type of automated testing doesn’t care about the implementation details, meaning how you wrote your code and how you implemented any feature or functionality. Unit testing only cares about the output of your code after you give it an input or interacting with the UI.

now let’s talk about the why

Why you would need to write tests? What benefits you’ll have? Isn’t just waste of time?

Any software that will be used by many users will need to do changes frequently. Maybe to fix a bug or add a new feature. Most of the software companies will do changes every day and push the code to production at least once a week and sometimes everyday. So it’s very easy and possible to break things on the way, so each time you will send a new version to the users you need to make sure everything still works as expected and nothing was broken unexpectedly and this is almost impossible to do manually due to the huge amount of things and scenarios you need to test.

So Unit tests will give you the confidence to ship your code frequently and Make sure you didn’t break anything on the way. Whenever you need to merge your code to production code or ship it to the users you’ll just run all the tests and right away you’ll know that something was broken or everything is still working fine.

what are the types of tests in flutter?

  1. Unit Tests
  2. Integration Tests
  3. Widget tests

To help you understand this we’ll take the example of a Car. Let’s assume we want to test whether or not our new car is working properly or not before shipping it to the Customer.

Unit Tests:

With unit tests, you can test a unit of your code maybe a function or a class. It doesn’t cover the interaction between classes or modules.

For the car examples it’s as if you’re taking the wheels or any part of the car and testing it separately in a seperate environment so not in the car itself. So at the end of the test we’ll know that our wheels are working properly, the engine also is working properly and it’s the same for all other parts.

The same applies for flutter unit tests. It won’t run on a real device or a simulator, it’s just another environment to test each unit of our code. So that’s why it’s very fast.

Integration Tests:

Integration tests gives you the ability to test your modules and the interaction between them, it will also run on a real device or simulator so you’ll see your app running as if you’re testing it manually. This type of test gives more confidence but it takes much more time and won’t be ideal to run in a CI/CD pipeline.

So let’s get back to the car example, this time you can imagine as if we’re putting back the engine and the wheels and testing both of them in the car and also the interaction between them. So when the engines runs the wheels also need to start rotating.

Widget Tests:

Actually widget tests in flutter gives you the ability to test your Widgets. Interact with them like clicking a button, writing in a text field and so on. You can use them to write unit tests if you want to test each widget seperately or you might also use it inside integration tests to test a hole module. So you can consider it as a tool from flutter to test your widgets, if you use it with unit tests it will run on a separate environment and if you use it with integration tests it will run a Real device/Simulator.

You can read more about types of tests here https://docs.flutter.dev/testing

Now let’s move the actually writing a test

This is the git repositories we’ll use through this series.

We’ll start by writing a simple unit test to test our CounterService.

Both test should be inside the test folder and should have _test.dart at the end so that when you run ‘flutter test’ it will recognize these tests and run them. The IDE also will recognise them this way.

NB: It’s a good convention to structure the test folder same way as the lib folder. This way it will be very easy to know what did you test and make it easy for anyone to read.

After you clone the repo, the code will be in main.dart. It’s easy to understand, it’s just a widget with 2 buttons and text. Each button will either increment or decrement. Using the Counter service which we’ll use just to understand the difference between unit test and widget test.

import 'package:flutter/material.dart';
import 'package:test_p1/services/counter_services.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Testing',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter testing'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final CounterService _counterService = CounterService();
void _decrementCounter() {
setState(() {
_counterService.decrement();
});
}

void _incrementCounter() {
setState(() {
_counterService.increment();
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${_counterService.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
FloatingActionButton(
onPressed: _decrementCounter,
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

The first test we’ll right, is a unit test to test our CounterService. As we said a unit test is just used to test a unit of logic. It might be a function, class..

In our case we’ll test our CounterService class. counter_service_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:test_p1/services/counter_services.dart';

void main() {
late CounterService counterService;
group('CounterService', () {
setUp(() {
counterService = CounterService();
});
test('should increment counter', () {
counterService.increment();
expect(counterService.counter, 1);
});
test('should decrement counter', () {
counterService.decrement();
expect(counterService.counter, -1);
});
});
}

In this test we’ll understand few things. First thing we need to add the main method which will be the first method to run.

Generally, first thing we start with in any test is to setup our dependencies. In this case we have one dependency which is CounterService.

So late CounterService is telling dart that we’ll make sure to initialise the counterService and it won’t be null.

The group(‘description’,(){}) will help us separate the tests into groups.

We have also few other helper methods such as:

setUp((){})

If you put this method inside the main method it will run before each test().

In our case we need a new instance for each test, so setUp will just help us do this.

If you put this method inside the group((){}) it will run before each test inside this group.

setUpAll((){})

It’s the same as setUp but it will run only once before the first test. If it’s inside main it will run before the first test only once. If it’s inside the group it will run only once before the first test inside this group.

tearDown((){})

same as setUp but it will run after the tests instead.

tearDownAll((){})

same as setUpAll but it will run after the last test.

test(‘Description’,(){})

This is where our test will be. You add a ‘Description’ describing the test and you just add the thing you’ll test and add an expect() method which will check whether the actual value is what we expected or not. In our case whether the counter will be 1 after we run the increment method or not. Same with decrement.

The expect method can have so many matchers we can use such as throwsA<Exception> or compare lists, maps and much more.

Running the tests

To run the tests we can either run the flutter command

flutter test 

Or you can also run them from the IDE ( example VS Code ). In the sidebar you’ll find this logo. You can run them either in debug mode so you can use breakpoints and everything in debug mode and you can see the results in the Debug Console or you can them in normal mode

You can also run each test separately with this tool or even with the command by specifying the file name.

You’ll also see this utility in vscode on top of each test.

Finally

We’ll finish the next type of test in a separate article. There are many things to teach in the widget test and it needs to be explained separately.

For now what we need to understand is that there are different types of tests.

We need to know what group, test, expect, setUp, setUpAll, tearDown, tearDownAll does and using these helpers you can write almost any unit test. Few advanced things such as testing streams or asynchronous code will be also explained later because this is just a beginners guide.

--

--

No responses yet