Unit testing

The motivation for writing unit tests shouldn't just be a simple way to test existing code, there are more benefits to consider:

  • The fundamental idea behind unit tests is to write isolated testable code. To achieve this, it makes developers write smaller single-purpose methods and therefore avoid writing long spaghetti-like code.
  • Refactoring existing code is easier because you can rewrite and test code in smaller portions. Refactored code should pass the same tests as the original code.
  • Tests themselves can be considered as documentation and examples of expected behavior.

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.

Note

If you're creating projects in Dart Editor, you can select the Console App template, and it adds the unittest dependency for you along with creating the basic directory structure.

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:

Matcher name

Description

isEmpty

Matches empty strings, maps, and collections using their isEmpty property.

throws

Tests whether a function or a returned Future object throws an exception.

equals

For iterables and maps, this tests all elements recursively. For anything else, this tests equality with ==.

completes

Matches a Future object that completes the execution successfully.

same

Matches whether actual and expected values are the same object.

isInstanceOf<T>

Matches when the actual object is an instance of <T>.

returnsNormally

Tests whether the function doesn't throw any exception. This also suppresses all exceptions thrown.

hasLength

Matches when the object has the length property, and its value is equal to the expected value.

contains

For strings, this checks for a substring; for collections, this checks for a matching element; and for map objects, this checks for existing key.

isIn

Tests whether the actual value is among expected values. The expected value can be a string, collection or a map.

equalsIgnoringCase

Matches when values are equal when compared case-insensitively.

orderedEquals

Matches whether the collection has the same number of elements and whether they are in the same order.

unorderedEquals

Matches whether the collection has the same number of elements in any order. Note that this method has the worst case complexity O2, which means that it can be very slow on a collection with larger number of elements.

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.

Unit testing

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:

Unit testing

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:

Unit testing

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:

  • --max or -m followed by a number. This is the maximum number that we'll check with isPrime() and hasUniqueDigits(). By default, it's set to 100.
  • --silent or -s, which is just true or false whether it's set or not. When true, print only the largest found number; otherwise, print all of them.

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:

Unit testing

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));
  });
} 

Asynchronous tests

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.

Test groups

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

Note the order of outer/inner setUp()/tearDown() functions.

Running all tests

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.42.128