Dart Unit Testing Basics

Madacode
7 min readJan 21, 2023

--

Quick unit test introduction. They’re basically a bunch of == statements

When you’re writing code, you often have to test it to see if your changes work or not. This manual testing process is often times very time consuming and distracting given the size of your codebase/app. You may wonder how to test your code consistently, without interrupting your train of thought and even automate some of the testing processes.

Enter Unit Testing. Unit testing allows you to reduce manual testing process in consistent, cheap and automatic manner. By unit testing you can:

  • See if your code works/is correct right through your IDE/Terminal window;
  • Adjust your code without having to go through the long and slow process of compilation and manually clicking the menus in the app;
  • Repeat those same tests many times for free and fast. 💨

By principle, unit testing is simple, it is basically comparing a function’s result with our predefined (manually made) expected result. The basic principles of a unit test are 3 A’s (Arrange, Act and Assert)

  • Arrange: create the input values based on the branches/value in the code and its expected result;
  • Act: actually calling the function, fire in the hole 🎉;
  • Assert: comparing the actual result from the tested function, see if that match our expectation.

To arrange the input values, we commonly go through one of this methods:

  • See for each branches in the code and make one unit test case per conditional branch;
  • See through the value ranges within a function and make a unit test for each value that falls in each value range.

Both of these principles are commonly called equivalence partitioning, a practice of finding the least amount of unit test cases to make while still maintaining the quality and coverage of those code branches.

And here are general rules of unit testing:

  • Unit test shall not access network/web services, any calls to such service shall be mocked accordingly;
  • Unit test shall not access file system for real, any access shall be mocked or kept to minimum extent possible;
  • Unit test shall not have timer delays built in, because unit tests are

Enough with the theories, now let’s get our hands dirty and dive down into unit testing in Dart/Flutter.

Note: do note that in this article we are learning unit test in Dart language, but skills for unit testing would transfer to other languages too, since they are principally the same, only the syntax and supporting libraries are different from one language to other.

Let’s start with the most basic function, that is simple addition, create a new file named add.dart and add the following code

int addNumbers(int num1, int num2) {
return num1 + num2;
}

Now, let’s create another file add_test.dart to contain our unit test file

import 'package:test/test.dart';
import './add.dart';

void main() {
test('shall add two numbers', () {
final int result = addNumbers(2, 2);
final int expected = 4;
expect(result, expected);
});
}

Do note that we have to import package:test/test.dart before making unit tests to get the basic test suite. Then, we make void main() function, as any Dart language program should then we began defining our test suite in test function call/clause, with first parameter being the test name and latter parameter being a function where the test is executed. Within the test execution function, we do the 3 A’s principle by:

  • Arrange: by defining the numbers to be added, that is 2 and 2 and defining the expected result that is 4;
  • Act: by calling the addNumbers function;
  • Assert: we call expect function to compare the actual result produced by addNumbers function vs our expected result value.

What if we have code branches, let’s move on to another example of doing unit test for functions with code branches:

Make a new file called is_even.dart and make a new function bool isEven(int x):

bool isEven(int x) {
if (x % 2 == 0) {
return true;
}

return false;
}

This code has 2 branches of execution:

  1. Where the number’s modulo against 2 is 0;
  2. When the number’s modulo against 2 ≠ 0.

With the branches defined in mind, let’s make a new file called is_even_test.dart to test our isEven function we just created.

import 'package:test/test.dart';
import './is_even.dart';

void main() {
test('shall return true given even number', () {
final bool result = isEven(200);
final bool expected = true;
expect(result, expected);
});
test('shall return false given odd number', () {
final bool result = isEven(201);
final bool expected = true;
expect(result, expected);
});
}

Do note that we make one unit test case for each conditional branches. We can also group up our unit test cases with group clause, with first parameter being the group name and the latter parameter being a function containing the unit test cases:

import 'package:test/test.dart';
import './is_even.dart';

void main() {
group('isEven unit test', () {
test('shall return true given even number', () {
final bool result = isEven(200);
final bool expected = true;

expect(result, expected);
});
test('shall return false given odd number', () {
final bool result = isEven(201);
final bool expected = true;

expect(result, expected);
});
});
}

Grouping may be useful if you are testing a class with lots of methods that you need to test one by one, therefore making the unit tests organized into group ‘folders’ by method name instead of having unit tests scattered around.

With the basics of unit test sorted out, let’s cover on assertion techniques. Assertion is kind of art on its own, while it looks kinda simple but given complexity of output data it may be a challenge on its own.

Let’s say we have a class named Person and a function that generates a Person object and you want to compare the output of such function with your expected output as shown in the example below in person.dart file:

class Person {
Person({
required this.name,
});

String name;
}

Person generatePerson(String name) {
return Person(name: name);
}

and with the unit test in person_test.dart file:

import 'package:test/test.dart';
import './person.dart';

void main() {
group('person unit test', () {
test('shall generate person', () {
final Person result = generatePerson(name: 'Mada');
final Person expected = Person(name: 'Mada';

expect(result, expected);
});
});
}

you may expect this to work, but instead you get an error screaming both objects are not the same while it is obvious that the objects are containing the exact same contents. Why is that?

The answer to it is that in object oriented languages (e.g. Java, Dart), objects by default are compared by its reference/pointer, since result and expected are defined in separate statements, its reference/pointer would be in different memory address. This type of comparison is named shallow comparison.

Thus, to enable deep comparison, where the objects are compared against its contents instead of its reference/pointer, we need to use equatable package from https://pub.dev/packages/equatable (follow the documentation on the link to install the package), then we need to extend our Person class with Equatable and define the list of props we want to expose for deep comparison.

class Person extends Equatable {
Person({
required this.name,
});

String name;

List<Object?> props => <Object?>[
name,
];
}

Now, try running the unit test again and it shall pass 👏.

What if we want to compare against a List, how do I compare that? For a list of simple values with exact same ordering, we can just expect it right away, like in this example (file name: list_n_squared.dart:

List<int> generateListNSquare(int n) {
return List<int>.generate(n, (int index) => index * index, growable: true);
}

This function will generate list of squared number from 0 to N. Now, let’s move to the unit test:

import 'package:test/test.dart';
import './list_n_squared.dart';

void main() {
group('generateListNSquare unit test', () {
test('shall generate list of squared number', () {
final List<int> result = generateListNSquare(3);
final List<int> expected = <int>[0, 1, 4, 9];

expect(result, expected);
});
});
}

Expecting the list will compare it by ordering and value (given list of primitive values). What about list of objects? We can do that so long as the object in the list extends from Equatable. Let’s take a look again from our Person example and make a function returning list of Person objects:

class Person extends Equatable {
Person({
required this.name,
});

String name;

List<Object?> props => <Object?>[
name,
];
}

List<Person> makeNPersons(String name, int count) {
return List<Person>.generate(
n,
(int index) => Person(name: '$name$count'),
growable: true,
);
}

This function will generate N copies of Person class, let’s make the unit test for makeNPersons function:

import 'package:test/test.dart';
import './person.dart';

void main() {
group('person unit test', () {
test('shall generate person', () {
final List<Person> result = makeNPersons('Mada', 3);
final List<Person> expected = <Person>[
Person(
name: 'Mada0',
),
Person(
name: 'Mada1',
),
Person(
name: 'Mada2',
),
];

expect(result, expected);
});
});
}

Since the Person class is extended from Equatable, now we can simply just expect both the result and expected value.

Last but not least, if you didn’t care about the ordering of list elements, we can use the package collection from https://pub.dev/packages/collection to help us, given our class is extended from Equatable. With collection package installed and imported, we can simply change the assertion shown in previous example with:

expect(
DeepCollectionEquality.unordered().equals(result, expected),
true,
);

We assert it against value true as the comparison method returns a bool.

What if asserting with aforementioned techniques produce an error that may shall not be an error (e.g. both lists are visually value-similar)?

Well, if this happens (usually for complex response, such as list of object with compound object/list), I really suggest breaking it down to set of simpler asserts, we can assert by one field, or making for loop to assert between our expected result vs actual result and so on.

Assertion on its own is an art and balancing act between under-asserting (that is skimping over important values to assert) and over-asserting (that is asserting every value, be it relevant or not to our test cases).

That’s all for unit testing basics in Dart language, I’ll see you on the next article on making unit test for our retrofit_dio_example's HomeViewModel. I’ll cover on how to mock deal with async unit tests.

Thanks for reading and see you in the next article 🙌.

--

--

Madacode

Senior Software Engineer @ Pinhome with 5+ years experience in various technologies including Go and Flutter. All posts are of my own opinion.