Unit testing Business Components in Flutter apps

The right way to do it

Mohammed Sadiq
Flutter Community
7 min readMay 27, 2020

--

Photo by Ash Edmonds on Unsplash

Unit testing has been part of Flutter since its inception. Two important dependencies easily integrate unit testing capabilities into your Flutter project. These dependencies (packages) are test and flutter_test.

In this article, you’ll learn how to write unit tests for business components of your project.

Business components solely deal with business logic and are almost always removed from UI logic.

Let’s get started.

Getting the basics right

Before starting, make sure that your pubspec.yaml file contains a similar set up as shown below under the dev_dependencies.

dev_dependencies:
flutter_test:
sdk: flutter

Folder structure

A good folder structure plays an important role in keeping your dev code separate from the testing code. There are a host of benefits in doing so.

  1. There is no unnecessary clutter in your lib folder.
  2. Separation of concerns. Your dev folders shouldn’t concern themselves with test files.
  3. Easy navigation and mental peace (😜).

An organization of folders, as shown below, upholds the above benefits well. Let’s take a look at it.

lib/
- xyz/
- xyz.dart
test/
- xyz/
- xyz_test.dart
  • Your folder structure under test and lib folders should be identical.
  • If your business component is within a file named xyz.dart under some lib/path/to/xyz folder.
    Then your test file for this component should be named xyz_test.dart and should be under the test/path/to/xyz folder.

Plan of attack

A framework for getting the tests right

GIF via GIPHY 🖤

Let us start with a very simple Calculator class.

class Calculator {
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int divide(int a, int b) {
return b == 0 ? 0 : (a ~/ b); // integer division
}
}

Although writing tests is an art in itself, many developers before us have systematized this process. It can be summarized as follows:

  1. Make a list of features of a given business component that you intend to test.
  2. For each chosen feature, jot down a series of subcomponents.
  3. Repeat step 2 recursively until you have reached a situation wherein no subcomponent can be broken down any further. You can term this process as atomization.
  4. Once all the subcomponents have been decided upon, pick each subcomponent one after the other.
  5. Make a list of expectations that you have from each atomic component.
  6. Write one test per expectation.

Let us apply and see

So coming back to our Calculator class, how do we go about testing it?

Adhering to the above framework, let us make a list of features.

  1. add
  2. subtract
  3. multiply
  4. divide

Each of these features is atomic from the start and thus we have already achieved steps 2 and 3.

Going forward, we should make a list of expectations for each subcomponent.

  1. add
    - two positives are added correctly.
    - two negatives are added correctly.
    - one positive and one negative is added correctly and vice versa.
  2. subtract
    - two positives are subtracted correctly.
    - two negatives are subtracted correctly.
    - one positive and one negative are subtracted correctly and vice versa.
  3. multiply
    - two positives are multiplied correctly.
    - two negatives are multiplied correctly.
    - one positive and one negative are multiplied correctly and vice versa.
  4. divide
    - two positives are divided correctly.
    - two negatives are divided correctly.
    - one positive and one negative are divided correctly and vice versa.
    - in the case that divisor is zero, zero should be returned.

Phew. We successfully jotted down all the expectations from each subcomponent.

For the sake of brevity, let us just write tests for the add feature.

What goes into the test files?

GIF via GIPHY 🖤

Remember the test files we spoke about earlier? Time to bring them out.
I’m assuming you have created a file called calculator_test.dart under the appropriate folder.

In accordance with the above procedure, we would have to write one test per expectation. Therefore we will have a total of four tests for the add feature. (Its really three but the last expectation requires us to swap the order of positive and negative so we update the count to four).

A sample test for the same will be written as follows:

test('should return a + b when a and b are two positives.', () {
// arrange
Calculator calculator = Calculator();
int a = 10;
int b = 20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

// assert
expect(actualResult, expectedResult);
});

test(String description, void Function() body) is a function provided by the test package.

Description of the test

The first argument test takes in is the description of the test. As can be seen in the above snippet, there is a convention being followed.

The convention is as follows:

should [expectation] when [condition].

The idea is to make the code read as:
The test component should do A when the condition is X.

Body of the test

Once again a pattern is being followed here. It is known as the AAA convention. Arrange. Act. Assert.

Arrange
This segment of the test body is where you set up facts of your test. Everything that you know is true for sure goes here. It also holds any initialization logic that your test requires. Lastly and most importantly, it contains the expectedResult variable (see the code above).

Act
You take action(s) here. And you assign the actualResult variable with a value returned from the component being tested under the given conditions. This is where the function calls go.

Assert
You make assertions here. You compare the actualResult with the expectedResult .
You can assert for equality, inequality, greater than, less than, and so on.

Following the AAA convention brings a lot of clarity in your tests and forces you to think objectively. As code quality should be maintained throughout the code base, your tests should be clean and tidy too.

Grouping the tests

We have established that we have four tests under the add component. Wouldn’t it be nice if we can club them together somehow? Kudos. You can absolutely do that. You are provided with a function called group . Once again, it comes from the test package.

group(String description, void Function() body) is very simple and its usage can be picked from the following snippet.

group('tests for add component', () {
test('should return a + b when a and b are two positives.', () {
Calculator calculator = Calculator();
// arrange
int a = 10;
int b = 20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

// assert
expect(actualResult, expectedResult);
});
});

group also takes a description parameter and a body parameter.

The convention for description is as follows:
tests for [component name]

It will make the code read as:
A group of tests for XYZ component.

body is pretty straight forward. It should contain a series of tests.

So in essence, a test suite for add would be as follows:

group('tests for add component', () {
test('should return a + b when a and b are two positives.', () {
// arrange
Calculator calculator = Calculator();
int a = 10;
int b = 20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

// assert
expect(actualResult, expectedResult);
});

test('should return a + b when a and b are two negatives.', () {
// arrange
Calculator calculator = Calculator();
int a = -10;
int b = -20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

//assert
expect(actualResult, expectedResult);
});

test('should return a + b when a is negative and b is positive', () {
// arrange
Calculator calculator = Calculator();
int a = -10;
int b = 20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

//assert
expect(actualResult, expectedResult);
});

test('should return a + b when a is positive and b is negative.', () {
// arrange
Calculator calculator = Calculator();
int a = -10;
int b = -20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

//assert
expect(actualResult, expectedResult);
});
});

Additionally, this body can have calls to setUp and tearDown functions.

setUp takes in a void Function() callback. This callback is called before running each test under a given group.
As can be seen from our above tests, each has repeatedly instantiated the Calculator class. This is exactly the kind of statement that should be part of the setUp .

setUp(() {
calculator = Calculator();
});

tearDown also takes in a void Function() callback. This callback is called after running each test under a given group. It should contain any cleanup code that has to run post each test.

tearDown(() {
// Any clean up tasks post each test go here.
});

Conclusion

In conclusion, the complete code to make these tests for add component work would be as follows:

import 'package:test/test.dart';

void main() {
group('tests for add component', () {
Calculator calculator;

setUp(() {
calculator = Calculator();
});

test('should return a + b when a and b are two positives.', () {
// arrange
int a = 10;
int b = 20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

// assert
expect(actualResult, expectedResult);
});

test('should return a + b when a and b are two negatives.', () {
// arrange
int a = -10;
int b = -20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

//assert
expect(actualResult, expectedResult);
});

test('should return a + b when a is negative and b is positive', () {
// arrange
int a = -10;
int b = 20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

//assert
expect(actualResult, expectedResult);
});

test('should return a + b when a is positive and b is negative.', () {
// arrange
int a = -10;
int b = -20;
int expectedResult = a + b;

// act
int actualResult = calculator.add(a, b);

//assert
expect(actualResult, expectedResult);
});
tearDown(() {
// Nothing to do here.
});
});
}

Writing tests in Flutter is extremely easy. And what’s more fun? The following screenshot.

Those green ticks ✅are love at first sight!

A full test suite for the above Calculator class can be found here.

--

--

Mohammed Sadiq
Flutter Community

An unwavering zeal to learn. To uncover. To reach out to the world in ways previously unimagined.