Friday, December 21, 2012

Dart Regression Testing for Hipsters

‹prev | My Chain | next›

A solid test suite was not an option for the first two editions of Dart for Hipsters. It is now. The advent of a proper unittest package in the Dart Pub means that I have no more excuses for incorrect code. More importantly, I have no excuse for not quickly identifying which code samples are out of date with the latest changes to Dart.

The question still remains, how do I test a code sample that looks something like the following?
    var str1 = "foo",
        str2 = str1;

    str1.hashCode; // 596015325
    str2.hashCode; // 596015325
(this demonstrates that string copies point to the original object)

I like the format of the code—it fits well in the narrative of the book. I would hate to introduce print() or other statements that might obscure the point. But as-is, that is not exactly a method that lends itself to testing. It's not even a method.

My first instinct is to put the code for the book inside a multi-line comment and then test similar code. That is less than ideal because I would need to work hard to keep the commented code in sync with the testing code.

In the end, I decide to put this inside a setUp block. The variables are local to that function, so I need a way to expose them for subsequent testing—library scoped variables to the rescue:
import 'package:unittest/unittest.dart';

String _str1, _str2;

main() {
  setUp((){
    /* ------------------------- */
    var str1 = "foo",
        str2 = str1;

    str1.hashCode; // 596015325
    str2.hashCode; // 596015325
    /* ------------------------- */

    _str1 = str1;
    _str2 = str2;
  });

  test('"foo" is 596015325', (){
    expect(_str1.hashCode, equals(596015325));
  });

  test('copy of "foo" is 596015325', (){
    expect(_str2.hashCode, equals(596015325));
  });
}
Using private variables to mirror the variables in the code sample is a choice of convenience rather than a need for them to be private. I get to use the same name, just with a leading underscore. This should make it obvious what is going on when I revisit this test a few months down the line.

The actual value of the hash code is not too important, so I may change it to a setUp variable in the future if I find that this causes trouble.

In the end, it is easy to visually see where the actual code snippet is. It is easy for the book software to grab that same extract. And I have a test that passes:
$ dart strings.dart                   
unittest-suite-wait-for-done
PASS: "foo" is 596015325
PASS: copy of "foo" is 596015325

All 2 tests passed.
unittest-suite-success
That is one snippet. How do I run multiple snippets at the same time? I cannot simply pass multiple dart tests to the dart interpreter because they would all have a main() entry point:
# Only the string concatenation tests run:
$ dart strings.dart string_concat.dart
unittest-suite-wait-for-done
PASS: "foo" is 596015325
PASS: copy of "foo" is 596015325

All 2 tests passed.
unittest-suite-success
I could put all of the tests into one big file. For various reasons, it would be better to keep the code snippets in separate files. In Dart, that means either import of libraries or including the separate files as a part of the whole.

I opt for import because that will allow me to use the same method name in each executable snippet. That is, instead of main() in my tests, I use run():
library strings_snippet;

import 'package:unittest/unittest.dart';

String _str1, _str2;

run() {
  group("[strings]", (){
    setUp((){
      /* ------------------------- */
      // code snippet here...
      /* ------------------------- */
      // ...
    });

    // tests here...
  });
}
Aside from the new run() method name, I also add a group call around my test to better distinguish the tests when all of them are run together. And, of course, I need a library statement at the top so that I can import the library into the main test.dart file.

As for the main test file, it is relatively simple:
import 'strings.dart' as Strings;
import 'string_concat.dart' as StringConcat;

main () {
  Strings.run();
  StringConcat.run();
}
Even though both libraries define a run() method, I can still import and use them thanks to Dart's library namespacing. The end result is quite readable—I rather like it.

And better still, I have some very nice test output verifying that my code snippets work with the most recent Dart:
$ dart test.dart
unittest-suite-wait-for-done
PASS: [strings] "foo" is 596015325
PASS: [strings] copy of "foo" is 596015325
PASS: [string concat] "foo" is 596015325
PASS: [string concat] "foo".concat("bar") is 961740263

All 4 tests passed.
unittest-suite-success
Well, at least the version of Dart from three days ago. Who knows what could be broken now? Actually I can—thanks to my nifty test suite.


Day #606

No comments:

Post a Comment