Thursday, December 20, 2012

Test Driven Error Fixes in Dart

‹prev | My Chain | next›

Thanks to the magic of dart_analyzer I know that my Dart Comics example Dart application is in exceptional shape. +Kasper Lund pointed out that my exception handling might be a bit off—despite the fact that dart_analyzer did not complain about it.

The problem is that in my haste to ensure that the remove() method in my event listener list class was not supported, I am not actually throwing an error:
abstract class HipsterEventListenerList implements EventListenerList {
  // ...
  EventListenerList remove(EventListener listener, [bool useCapture=false]) {
    throw UnsupportedError;
  }
  // ...
}
Instead of throwing an error, I am throwing an error class. That is allowed per the dart language spec. Any old object can get thrown—it does not have to be an exception or error subclass. Indeed it seems that I can even throw classes. But I do not believe that it is possible to catch that error.

To test that theory, I write a test. This particular class resides in the Hipster MVC package, so I will add my test there.

I am going to need a browser for this test since the event classes are based on the events in the dart:html library, which requires a browser (and there are no headless Dart environments yet). The skeleton of the test imports the unittest library, the enhanced HTML reporter (for pretty browser reporting), and the events library:
import 'package:unittest/unittest.dart';
import 'package:unittest/html_enhanced_config.dart';

import 'package:hipster_mvc/hipster_events.dart';

class TestEventListenerList extends HipsterEventListenerList {}

main() {
  useHtmlEnhancedConfiguration();
  // tests go here...
}
I need a concrete class to test the remove() method in the abstract HipsterEventListenerList, hence TestEventListenerList above. In the main() entry point, I start the HTML reporter.

As for the test itself, I define a group in which I will describe all unsupported methods (currently only remove). Groups are not strictly required by the testing library, but they help with the output. In the test proper, I create an instance of TestEventListenerList so that I can test it. As for my expectation, I expect the result of invoking an anonymous function that removes a dummy value from the list will throw an unsupported error:
main() {
  useHtmlEnhancedConfiguration();

  group('unsupported', () {
    test('remove', (){
      var it = new TestEventListenerList();
      expect(
        () => it.remove("foo"),
        throwsUnsupportedError
      );
    });
  });
}
If I do not use an anonymous function inside the expect function, then it.remove() would get evaluated before the expect call. That is, the following would not work:
    test('remove', (){
      var it = new TestEventListenerList();
      expect(
        it.remove("foo"),
        throwsUnsupportedError
      );
The it.remove("foo") would throw an UnsupportedError before expect even has a chance to evaluate its two parameters.

As an aside, I think that I appreciate the distinction between errors (non-recoverable) and exceptions (exceptional occurrences, but recoverable). In this case, code that expects that an event listener was removed would almost certainly be in an unstable state, which should crash the application. Hopefully an intrepid contributor would then feel compelled to submit a patch.

Anyhow, back to my original problem. What happens when I run this test against my code that throws a class instead of an object? The test crashes, dumping the following into the console:
Internal error: 'file:///home/chris/repos/hipster-mvc/test/packages/hipster_mvc/hipster_events.dart': Error: line 32 pos 11: illegal use of class name 'UnsupportedError'
    throw UnsupportedError;
          ^ 
Ah! So dart_analyzer might give me a pass on this, but the Dart VM in Dartium does not.

The next step is easy enough: I have my failing (well, not compiling) test. Let's make it pass. The fix is similarly easy—I just need to throw an instance of UnsupportedError:
abstract class HipsterEventListenerList implements EventListenerList {
  // ...
  EventListenerList remove(EventListener listener, [bool useCapture=false]) {
    throw new UnsupportedError("Planned in a future release");
  }
  // ...
}
With that, I have my first Hipster MVC test passing:


I appreciate being able to drive this bug fix with tests, which are easy in Dart since the unittest library is available as a Dart Pub package. Hopefully this particular test will not be long-lived, and will be replaced by a test that verifies removal of an event listener. Still, it is nice to have the ability to drive functionality with a reusable test (of course, I would still prefer a way to do so headless).

I also really like the test matchers that are built into the unit testing library. It is nice being able to write throwsUnsupportedError rather than setting my expectation on the outcome of a try-catch block. Reporting—especially when tests fail—is much nicer with matchers like this.


Day #605

No comments:

Post a Comment