Wednesday, September 18, 2013

Testing Canvas in Dart


While working through some of the later chapters in Dart for Hipsters, I found some code that really was not tested. Better yet, I know why it is untested: I don't know how. Yay! Fresh earth.

The Dart code in question is part of a whirlwind tour at the end of the book. Specifically, I have a very simple canvas demonstration:



The code draws a simple square, fills it, and attaches keyboard handlers to move the square around the screen. I already know that I cannot test the keyboard events. At least not yet. But I can still test something. In fact, if I had been testing, or at least performing simple code analysis, I would have noticed that I still have the old-style on.keyDown.add() event handling:
  document.
    on.
    keyDown.
    add((event) {
      String direction;

      // Listen for arrow keys
      if (event.keyCode == 37) direction = 'left';
      if (event.keyCode == 38) direction = 'up';
      if (event.keyCode == 39) direction = 'right';
      if (event.keyCode == 40) direction = 'down';
      // ...
    });
I get everything working by switching to the new stream-based event handling:
  document.
    onKeyDown.
    listen((event) { /* ... */ });
But I have nothing in place to prevent regressions should the language continue to evolve.

This turns out to be fairly easy with Dart's unittest:
import 'package:unittest/unittest.dart';
import 'dart:html';

import '../web/main.dart' as Canvas;

main () {
  group("[canvas]", (){
    var canvas;

    setUp((){
      canvas = new CanvasElement();
      document.body.append(canvas);
    });

    tearDown(()=> canvas.remove());

    test("code runs", (){
      expect(Canvas.main, returnsNormally);
    });
  });
}
Here, I import the main.dart entry point for my canvas experiment and manually run the main() entry point function—expecting it to return normally. A bit of setup is required to add an expected <canvas> tag (along with matching teardown code). But really, very little is required for a pretty useful test.

It even works from the command-line thanks to the magic of Chrome's content_shell:
➜  code git:(master) ✗ content_shell --dump-render-tree index.html
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: [canvas] code runs
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 1 tests passed.
If I intentionally break the code (by reverting my stream fix), I get the failure that would have helped me earlier:
➜  code git:(master) ✗ content_shell --dump-render-tree index.html
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: FAIL: [canvas] code runs
  Expected: return normally
    Actual: <Closure: () => dynamic from Function 'main': static.>
     Which: threw NoSuchMethodError:<Class 'Events' has no instance getter 'keyDown'.

  NoSuchMethodError : method not found: 'keyDown'
  Receiver: Instance of 'Events'
  Arguments: []>

  package:unittest/src/simple_configuration.dart 140:7
  package:unittest/src/simple_configuration.dart 15:28
  package:unittest/src/expect.dart 117:9
  package:unittest/src/expect.dart 75:29
  ../html5/test/test.dart 20:13
My code event passes type analysis, which I perform automatically on any tested code in the book with dartanalyzer in my test_runner.sh:
# ...
# Static type analysis
echo
echo "Static type analysis..."

results=$(dartanalyzer test.dart 2>&1)
echo "$results"
if [[ "$results" == *"[error]"* ]]
then
    exit 1
fi
# ...
That is all well and good, but is it possible to test some of the innards of the <canvas> element after the initial screen is drawn by main()? As can be seen from the screenshot above, the “player” starts near the top-left of the screen. In fact, the default is specified in the Player constructor:
class Player {
  int x, y;
  int width = 20, height = 20;
  Player() {
    x = width~/2;
    y = height~/2;
  }
  // ...
}
The x-y origin of the starting position is then (20/2, 20/2), or (10, 10). Given that the width and height of the box is 20×20, the box ends at (30, 30). Since the “player” box is the last thing drawn in the <canvas> element, I can have an expectation that the point (30, 30) is in the current path:
    test("player is drawn", (){
      Canvas.main();
      var context = canvas.getContext('2d');
      expect(context.isPointInPath(30, 30), isTrue);
    });
And that passes!
PASS: [canvas] player is drawn 
If I change the expectation to (31, 31), my test fails:
FAIL: [canvas] player is drawn
  Expected: true
    Actual: <false>
So not only do I have a test that will catch small changes in the language that might affect my <canvas> code, I also have a test that will verify that my drawing works properly. And it even works from the command line.


Day #878

No comments:

Post a Comment