The motivation for writing unit tests shouldn't just be a simple way to test existing code, there are more benefits to consider:
For this example, we'll write a console application that finds all numbers that are prime numbers, and all its digits are used only once. This is, for example, the number 941, because it's a prime number and contains each digit only once. On the other hand, the number 1,217 is a prime number but contains the digit 1 twice, so that's not what we're looking for.
Then, we'll extend our example with another method that returns a list of all prime numbers smaller that a certain maximum.
We'll start by creating a new Dart project and adding the unittest
dependency. However, this time, we're not going to add it in the dependency
directive but rather dev_dependencies
. Both of these have the same meaning but the second one is pulled by the pub
tool only when your package is not pulled as a third-party dependency. In other words, if you run pub get
on this package, it will also download dev_dependencies
, but if you use it as a dependency of another package, it won't be downloaded because it's not required by this package for regular usage. The dev_dependencies
directive is useful mostly to specify packages that are necessary for developing or testing the package. Also, we can add another dependency called args
, which is a library for parsing command-line arguments that we've already seen in the previous chapter, and we'll use it here later.
First, we'll create a method that checks whether an integer is a prime number, but for testing purposes, we'll introduce a bug in it:
// lib/methods.dart import 'dart:math'; import 'dart:convert'; bool isPrime(int num) { // Intentionally broken, number 2 is a prime. if (num <= 2) { return false; } else if (num % 2 == 0) { return false; } for (int i = 3; i < pow(num, 0.5).toInt() + 1; i += 2) { if (num % i == 0) return false; } return true; }
Then, we write our test code that uses the unittest
library and is runnable from the console:
// test/test_is_prime.dart import 'package:unittest/unittest.dart'; import 'package:unittest/vm_config.dart'; import 'package:Chapter_08_unittest/methods.dart'; void main() { // Tell unit test library about our output format. useVMConfiguration(); defineTests(); } void defineTests() { test('3 is a prime', () { expect(isPrime(3), isTrue); }); test('2 is a prime', () { expect(isPrime(2), isTrue); }); }
A test is defined by calling the test()
function, which comes from the unittest
library and defines a single test case. It takes two arguments, the name of the test, and an anonymous function that represents the test's content with one or more expect()
calls.
Each expect()
function takes actual and expected values as arguments, respectively. The expected value is a subclass of the Matcher
class, which is responsible for resolving whether these two match.
In our case, we have a single expect()
call that checks whether the value returned from isPrime(3)
is equal to the Boolean true
value. The isTrue
instance is a constant defined in unittest
package. As you might have guessed, there are quite a lot of predefined matchers.
The most commonly used matchers are isMap
, isList
, isNull
, isNotNull
, isTrue
, isFalse
, isNaN
, and isNotNaN
—which are pretty self-explanatory. The following table has more predefined matchers:
There are also specialized matchers for integer ranges, particular exceptions, iterables, and maps, and you can define your own matchers as well. For a complete list of all existing matchers, refer to the API reference at http://www.dartdocs.org/documentation/matcher/0.12.0-alpha.0/index.html#matcher/matcher.
We can run our test from the console. The useVMConfiguration()
function tells Dart unit testing library what output format we want and what exit status code to use. We know that our isPrime
function has a bug, so we're expecting it to fail.
We could also run unit tests in a browser by including another configuration:
import 'package:unittest/html_config.dart';
After this, we would have to use useHtmlConfiguration()
instead. Note that only one configuration can be used at a time.
You can safely ignore the first line, unittest-suite-wait-for-done
, which is a message for the environment running these tests.
Then, we see that the first test passed but the second test failed because it returned a false
Boolean while the expected value was true
. Although the second test failed, it's not an error. In other words, the tested function ran correctly but didn't return the expected value.
With echo $?
, which is a standard Unix command to return the status code for the previous command, we see that it returned 1 because one of the tests failed or threw an error. If all tests passed, the return code would be 0
(values 0 and 1 are standard for success and error states, respectively, in Unix-based systems).
We can simulate an error by, for example, changing the function definition to isPrime(var num)
and then testing with expect(isPrime('foo'), isTrue)
. Changing int num
to var num
bypassed the static type check and we might not even notice that the rest of isPrime
expects its parameter to be an integer.
Running the tests this time throws an error instead of just failing the test:
Let's fix the isPrime()
function:
/* … */ bool isPrime(int num) { if (num < 2) { return false; } else if (num == 2) { return true; } else if (num % 2 == 0) { return false; } for (int i = 3; i < pow(num, 0.5).toInt() + 1; i += 2) { if (num % i == 0) return false; } return true; }
The following screenshot shows the output of the fixed code:
Finally, both tests passed and the return code is 0
.
The second function that we're going to write is called hasUniqueDigits()
. It takes an integer as a parameter and returns false
if any of the digits appear more than once:
// lib/methods.dart import 'dart:convert'; bool hasUniqueDigits(int num) { String numAsStr = num.toString(); Set<int> bytes = UTF8.encode(numAsStr).toSet(); return bytes.length == numAsStr.length; }
We convert the integer to a String
object, then we convert it to List<int>
with the encode()
method, and then to Set<int>
. A set can contain each value only once, so if the list contains two same digits, then the lengths of the set and the length of the string won't match.
The tests for this function are basically the same as the tests for isPrime()
:
// test/has_unique_digits_test.dart import 'package:Chapter_08_unittest/methods.dart'; import 'package:unittest/unittest.dart'; import 'package:unittest/vm_config.dart'; void main() { useVMConfiguration(); defineTests(); } void defineTests() { test("doesn't have unique digits", () { expect(hasUniqueDigits(123441), isFalse); }); test('has unique digits', () { expect(hasUniqueDigits(42), isTrue); }); }
When we put everything together, we'll create a command-line application that takes two arguments:
The args
library that we added as a dependency earlier will do all the parsing for us:
// bin/unique_and_prime.dart import 'package:args/args.dart'; import 'package:Chapter_08_unittest/methods.dart'; void main(List<String> args) { final parser = new ArgParser(); parser.addOption('max', abbr: 'm', defaultsTo: '100'), parser.addFlag('silent', abbr: 's'), ArgResults argResults = parser.parse(args); int max = int.parse(argResults['max']); bool silent = argResults['silent']; int maxNum = 0; for (int i = 1; i < max; i += 2) { if (isPrime(i) && hasUniqueDigits(i)) { maxNum = i; // Print all found numbers only when silent isn't true. if (!silent) print("$i"); } } // When silent is set, print the max found number. if (silent) { print("$maxNum"); } }
We can run this application by issuing the following command:
$ dart unique_and_prime.dart
This command uses default values for --silence
and --max
and prints each number on a new line.
$ dart unique_and_prime.dart --silent -m 1000000
With this command, we check the first million integers (actually, we increment by 2, so that's just half of them), and if we consider that we check for primes in another loop, that's quite a lot of operations. To measure processing time, we can prepend the console command with time
and see that it actually runs reasonably fast:
We can add one more function to lib/methods.dart
. Let's say we want to use our isPrime()
function to generate a list of first n primes in the same order in which they were found:
List<int> getPrimes(int total) { List<int> found = new List<int>(); int num = 1; while (found.length != total) { if (isPrime(num)) { found.add(num); } num++; } return found; }
To check the correct results, we can use the orderedEquals
matcher:
void main() { useVMConfiguration(); defineTests(); } void defineTests() { test('first 10 primes', () { var actual = getPrimes(10); List<int> expected = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]; expect(actual, isList); // This test is redundant, orderedEquals() checks it for us. // expect(actual, hasLength(10)); expect(actual, orderedEquals(expected)); }); }
If we wanted to test code that's called asynchronously, such as Ajax, filesystem operations, or basically all IndexedDB functions, we need to use the expectAsync()
function instead of expect()
. The reason is that unittest
doesn't know that we're not interested in the actual function call that makes an asynchronous call but are rather interested in its callback. Therefore, this example won't do what we want:
// test/test_async.dart void defineTests() { // This is wrong, don't do this. test('test async', () { int actual; new Timer(new Duration(milliseconds:1000), () { print('callback fired!'), actual = 42; }); expect(actual, equals(42)); }); }
Then, when we run this test, it returns immediately and reports a failed test with:
FAIL: test async Expected: <42> Actual: <null>
Instantiating a Timer
class schedules a callback to be called 1
second in the future, but unittest
doesn't know about it, and it thinks that this is all you wanted to do.
In order to fix this, we'll wrap the Timer
class's callback with the expectAsync()
call and pass the original callback to it:
test('test async callback', () { new Timer(new Duration(seconds:1), expectAsync(() { print('callback fired!'), int actual = 42; expect(actual, equals(42)); })); });
When we run the test again, it works as we wanted:
$ dart test_async.dart callback fired! PASS: test async All 1 tests passed.
The expectAsync()
function can take optional count
and max
parameters that will make the test pass only if you call its callback an exact number of times or the maximum number of times, respectively.
Note that tests can make use of the async
and await
keywords from Dart 1.9 just like any other Dart app.
Usually, it makes sense to group tests into logic blocks. This means putting tests that check the same or similar functionality together to make the output more readable and easily trackable in case it goes wrong. Groups can be also nested.
When using groups, we can make use of two more functions: setUp()
and tearDown()
. These are used to prepare local variables, such as instantiating objects or loading fixture data, and are called before and after each test (note that tests shouldn't rely on a state created by other tests). The setUp()
and tearDown()
methods can be nested just like groups and are called from the outermost to innermost for setUp()
and the other way around for tearDown()
. We can try to make our test_is_prime.dart
file a little more complicated:
void defineTests() { group('True expected:', () { setUp(() => print('Outer setUp')); tearDown(() => print('Outer tearDown')); group('Lower bound:', () { setUp(() => print('Inner setUp')); tearDown(() => print('Inner tearDown')); test('3 is a prime', () { print('3 is a prime test content'), expect(isPrime(3), isTrue); }); test('2 is a prime', () { print('2 is a prime test content'), expect(isPrime(2), isTrue); }); }); test('12197 is a prime', () => expect(isPrime(12197), equals(isTrue))); }); group('False expected:', () { test('21 is not a prime', () => expect(isPrime(21), isFalse)); test('21357 is not a prime', () => expect(isPrime(21357), equals(false))); }); }
See the order of setUp()
and tearDown()
functions. The first setUp()
function is called for the outer group, then the inner, and after that, it calls the test itself. For tearDown()
functions, the first group called is the inner and then the outer. The same procedure is applied for each test in the group:
$ dart test_is_prime.dart Outer setUp Inner setUp 3 is a prime test content Inner tearDown Outer tearDown Outer setUp Inner setUp 2 is a prime test content Inner tearDown Outer tearDown Outer setUp Outer tearDown PASS: True expected: Lower bound: 3 is a prime PASS: True expected: Lower bound: 2 is a prime PASS: True expected: 12197 is a prime PASS: False expected: 21 is not a prime PASS: False expected: 21357 is not a prime
Every test that we defined in the test/test_*.dart
files has its own main()
function that calls its defineTests()
function, but calling each test by ourselves isn't very practical, so we'll create an all.dart
Dart script that imports all tests and runs them at once:
// test/all.dart import 'package:unittest/vm_config.dart'; import 'test_is_prime.dart' as test_is_prime; import 'test_has_unique_digits.dart' as test_has_unique_digits; import 'test_get_primes.dart' as test_get_primes; import 'test_async.dart' as test_async; void main() { useVMConfiguration(); test_is_prime.defineTests(); test_has_unique_digits.defineTests(); test_get_primes.defineTests(); test_async.defineTests(); }
At the end, running all tests is easy:
$ dart all.dart
Note that tests are called in the order in which they are defined.
52.15.42.128