Holy Cow, Dart Unit Tests are Easy



Dart provides a rich unit test library out of the box. Inspired by testing libraries like Hamcrest and dartmatch, the dart:unittest library provides test groups, numerous matchers, and more. Read on to learn how easy it is to write your first Dart unit tests!

You, too, can create tests this pretty.

Test subject

To illustrate, I've just created the soon-to-be massively critical embiggen library. This library will bigify your text. It's very advanced.

Of course, I made a pub package for this library:


The contents of embiggen.dart are:

 library embiggen;  
   
 String embiggen(String msg) {  
  if (msg == null) {  
   throw new ArgumentError("must not be null");  
  }  
    
  return msg.toUpperCase();  
 }  

I'll let that sink in for a while, it's pretty intense.

Add the unit test package

You can use pub to get the unit test package and link it into your project. Simply declare the unittest dependency in your pubspec.yaml:

 name: embiggen  
 description: Turns small things into big things.  
   
 dependencies:  
  unittest: any  

Dart Editor will automatically run "pub install" when it detects a change to pubspec.yaml. If you are using the command-line tools, you can run "pub install" manually. This step will find the unittest package in your local pub cache, or it will download it from pub.dartlang.org. Once found, pub will set up the proper symlinks to make unittest available to your project.

Set up the tests

The pub package layout convention dictates that tests should be placed inside a "test" directory. Create a new file named "embiggen_test.dart" and place it inside "test".

Import both the embiggen library and the unittest library. You need to import embiggen because your test needs to access the actual embiggen function.

Here is embiggen_test.dart so far:

 library embiggen_test;  
   
 import 'package:embiggen/embiggen.dart';  
 import 'package:test/test.dart';  
   

(Note: you may need to run "pub install" again, which will configure symlinks in your "test" directory.)

Write a test

Dart is a language without much ceremony, so let's dive into it. Create a main() function and place the first test inside. A test takes a name (or, a very short description) and an anonymous function that itself contains the test condition.

Here is embiggen_test.dart with a simple test:

 library embiggen_test;  
   
 import 'package:embiggen/embiggen.dart';  
 import 'package:test/test.dart';  
   
 main() {  
  test("bigifies text", () {  
   expect(embiggen("hello"), equals("HELLO"));  
  });  
 }  

Use "expect(actualValue, expectedValueMatcher)" to perform the actual test. The "expectedValueMatcher" is dynamic, it can be a simple value or a matcher that can test for a wide variety of values. In the above example, equals("HELLO") returns a simple matcher that matches on equality. More on matchers shortly.

Run the test

Choosing to place the tests inside main() was deliberate, it enables the Dart file to act as its own test runner. Simply run the embiggen_test.dart file, either from the command line or via Dart Editor.

Here is the output on the console:

 unittest-suite-wait-for-done  
 PASS: bigifies text  
   
 All 1 tests passed.  
 unittest-suite-success  

Out of the box formatting!

A failed test looks like this:

 unittest-suite-wait-for-done  
 FAIL: bigifies text  
  Expected: 'HELL'  
     but: was 'HELLO'.  
    
  #0   DefaultFailureHandler.fail (package:unittest/src/expect.dart:86:5)  
  #1   DefaultFailureHandler.failMatch (package:unittest/src/expect.dart:90:9)  
  #2   expect (package:unittest/src/expect.dart:55:29)  
  #3   main.<anonymous closure> (file:///Users/sethladd/dart/embiggen/test/embiggen_test.dart:8:11)  
  #4   TestCase.run (package:unittest/src/test_case.dart:83:11)  
  #5   _nextBatch._nextBatch.<anonymous closure> (package:unittest/unittest.dart:808:19)  
  #6   guardAsync (package:unittest/unittest.dart:767:19)  
    
   
 0 PASSED, 1 FAILED, 0 ERRORS  
   

Test for exception

Passing null to embiggen() will throw an ArgumentError, so let's not forget to test for the error condition. First, be sure to wrap the condition to test inside an anonymous function. This ensures that any exceptions thrown by the condition are properly caught. There are plenty of pre-built matchers to help with exceptions, from the simple "throws", which tests that anything was thrown, to specific matchers like "throwsArgumentError".

Here is embiggen_test.dart with a test for an exception:

 library embiggen_test;  
   
 import 'package:embiggen/embiggen.dart';  
 import 'package:test/test.dart';  
   
 main() {  
  test("bigifies text", () {  
   expect(embiggen("hello"), equals("HELLO"));  
  });  
    
  test("null throws ArgumentError", () {  
   expect(() => embiggen(null), throwsArgumentError);  
  });  
 }  

What's next

This post barely scratches the surface. Dart's unit test library has support for setup/teardown, test groups, nested groups, a very extensive matchers library, and even mocks/stubs. Dart also has various test outputs, ranging from VM to HTML (which you can see an example of in the top of this post).

Further reading


Summary

Unit tests come out of the box with the Dart SDK. They are simple to write, compose well, and have many matchers. Simply create a "test" directory, add Dart test scripts, and BOOM you're making ice cream.

(Nerd cow credit: http://www.flickr.com/photos/toastersarts/6893702709/)

Popular posts from this blog

Lists and arrays in Dart

Converting Array to List in Scala

Null-aware operators in Dart