
Unit testing
If you are a Test Driven Development advocate you are probably a bit concerned that we have not written any unit tests so far. That is about to change as we introduce the Dart unit test package, which is simply called test
(you may see older example code online that uses the older unittest
package).
In the sample code for this chapter, there is a sub-folder called Unittest
that contains a unit test in bin/main.dart
:
library Unittestdemo.test; import 'package:test/test.dart'; void main() { test('HelloWorldTest', () { expect(1+1, 2); }); }
This defines a new test called HelloWorldTest
and the actual test can be carried out by the past in function. The result of the test can be validated using the expect
method. The library contains an extensive range of matchers to check a result, for example, isResult
, isList
, isNull
, isTrue
and isNonPositive
.
Note
Unit Testing focuses on testing a small part of an application at a time. Tests are defined in code, so that they can be run easily and quickly. To ensure they run quickly, true unit tests should not use external resources such as the network or files.
For the code behind an application to be easily unit tested, the design has to allow for the objects to be tested in isolation. For example, all IO may be wrapped into a simple class so that a unit test can be written that uses a fake, or mock, version of the IO class.
This impacts the design but makes sure the code is tested all the way through development. Tests can even be written before the development starts.
Running unit tests
Unit tests are run like any other Dart command line program from your chosen IDE/Editor or from the command line. For instance, from the Unittest
folder the test can be run as dart bin/main.dart
:
00:00 +0: HelloWorldTest 00:00 +1: All tests passed!
The default output for the test runner includes color characters (which may not be supported in your IDE or shell):

The output shown gives the names of the test run and the overall result:
00:00 +0: HelloWorldTest 00:00 +0 -1: HelloWorldTest Expected: <33> Actual: <2> package:test expect bin/main.dart 9:5 main.<fn> 00:00 +0 -1: Some tests failed.
Of course, it doesn't always go well and detail is needed to go and investigate any issue. As well as showing the failing test detail, an exception is thrown so the process running the tests will exit with an error.
Writing unit tests for the data monitor
To get a head start on the next part of the project, we will put together some tests for a class to deal with the contents of the JSON. This is in the test
folder of the QuakeMonitorDB
project. The Dart Analyzer will show some issues with these files:
Warning:(16, 15) Undefined class 'GeoUpdate'
This is because we are writing our tests first, in true TDD (Test Driven Development).
The file test/qmdb_test.dart
contains the test cases, and test/data.dart
has some sample data for our tests. Having a fixed set of data is typical for unit testing, as it gives predictable results for the test and does not rely on external resources:
test('Object create.', () { var o = new GeoUpdate(); expect(o, isNotNull); });
The first test is a simple creation of the object with no parameters. It is useful to break tests down to this level, as when future changes to the code makes the tests fail, it will be easier to identify at what point the failure occurred.
Note
The sample data for testing is declared as const
. What's the difference between final
and const
? That's a good question!
A final
variable is single-assignment and is required to have an initializer. No further changes can be made.
A const
object is frozen and completely immutable at compile time.
The keywords final
and const
provide valuable hints to the compiler about what a variable is, and how it will be used, which can aid performance.
It is always good to be organized, and as we may end up with hundreds, if not thousands of unit tests in a large project, they can be grouped together:
group('GoodJSON', () { var o; setUp(() { o = new GeoUpdate(sampleJson); }); test('T1. Simple load.', () => expect(o.src, equals(sampleJson))); test('T2. Blank load', () { o = new GeoUpdate(""); }); });
To run tests, there is sometimes a common set of steps to initialize test objects. The test package provides this facility via the setUp
function, which is run before the tests in the group.
The final group of tests focus on Features,
which is the name used for the main earthquake entries in the geoJSON feed:
group('Features', () { var o; setUp(() { o = new GeoUpdate(sampleJson); }); test('Test 1', () => expect(o.features.length, equals(5))); test('Test 2', () => expect(1, equals(1))); });
Developing tests and the implementation is usually an iterative affair during the development of a project. As the second test in this groups shows, it is easy to put in a placeholder test. Tests help ensure that the final implementation of the code has an upfront design, is loosely coupled, and also provides an easy way to automatically retest a range of functionalities when a change is made.
Contrast running a set of unit tests in a few seconds covering many test cases, taking a few minutes to run a full web application, browsing to a page, entering data, checking the result, and only having covered a single test case.
Running the unit tests produces results that show all the tests are failing. This is expected as we have not written the implementation yet.
We are not ending the chapter on a low note! What we have done is considered the design, defined some requirements for the object, and created a test suite to validate the solution as we create it.