Tuesday, September 17, 2013

A Test Not Run is a Worthless Test


I must have detached the bulk of my Dart for Hipsters tests from the main test suite back when I first started on the latest rewrite. Since that was such a long time ago, I have since forgotten, leaving me with a bunch of tests that are not running. In other words, I have a bunch of worthless tests.

As I work through each chapter's worth of tests, hooking the old tests back up, I am pleasantly surprised when they pass. At the same time, I am eager for failures because that means that something may have changed—something may be new to me. Tonight, as I was working through the chapter on dynamic language features, I ran into some problems with my noSuchMethod() code.

Not wanting a repeat of last night's debacle in which I very nearly repeated a post, tonight I search for and find the original post on this code. That does not include the somewhat recent switch from invocation “mirrors” to a plain-old invocation objects. That introduction article does a pretty good job of explaining the errors that I am seeing in my tests:
ERROR: [noSuchMethod] can pass parameters to noSuchMethod in superclass
  Test failed: Caught NoSuchMethodError : method not found: 'bar'
  Receiver: top-level
  Arguments: [2, 2]
  ../varying_the_behavior/test/superclass_no_such_method.dart 13:5                                                                          A.noSuchMethod
  ../varying_the_behavior/test/superclass_no_such_method.dart 28:30                                                                         C.noSuchMethod
  ../varying_the_behavior/test/superclass_no_such_method.dart 46:19                                                                         run..
  package:unittest/src/test_case.dart 111:30  
My code is actually reaching the throw statement in my noSuchMethod() even though my test is trying to invoke the bar() method:
class A {
  noSuchMethod(args) {
    if (args.isMethod && args.memberName == "bar") {
      return 2 * args.
        positionalArguments.
        reduce(0, (prev, element) => prev + element);
    }

    throw new NoSuchMethodError(
      this,
      args.memberName,
      args.positionalArguments,
      args.namedArguments
    );
  }
}
The problem is that memberName is no longer a string (“bar”). It is now a Symbol version of that string. It is interesting what a language sometimes has to do to itself when it has to compile into a another language. In this case, my Dart test code needs to work when compiled down to JavaScript:
    test('can access noSuchMethod in superclass when defined in subclass', (){
      var c = new C();
      expect(()=> c.bar(2, 2), returnsNormally);
    });
The problem, as the announcement article explains so well, is not so much that the above code can be compiled to JavaScript. Rather the problem is that the above test code might get compiled into minified JavaScript. In other words, the bar() method might get minified as a() in which case a string-based memberName would return “a” when my noSuchMethod() definition supports only “bar.”

Enter Symbol, which is treated differently in by the dart2js compiler. To put that to use in my noSuchMethod() definition, I compare memberName, which is a Symbol, to a Symbol object that I create:
class A {
  noSuchMethod(args) {
    if (args.isMethod && args.memberName == const Symbol("bar")) {
      return 2 * args.
        positionalArguments.
        fold(0, (prev, element) => prev + element);
    }
    // ...
  }
}
That is all that I need to do in order to make my test pass.

I am not quite done, though. Happily the invocation announcement article also includes a way to clean up my messy, six line throw-no-such-method throw statement. The bar() method is the only method that I support via noSuchMethod() in this class, so when something else is called, I need to raise an error which I had been doing with:
class A {
  noSuchMethod(args) {
    if (args.isMethod && args.memberName == "bar") { /* ... */ }

    throw new NoSuchMethodError(
      this,
      args.memberName,
      args.positionalArguments,
      args.namedArguments
    );
  }
}
Mercifully, there is a better way—by invoking noSuchMethod() on super:
class A {
  noSuchMethod(args) {
    if (args.isMethod && args.memberName == const Symbol("bar")) { /* ... */ }

    return super.noSuchMethod(args);
  }
}
In other words, let Object, or whatever other superclass might be involved, deal with the args Invocation object. Yay! Much nicer.

I cannot say that I am 100% sold on the idea of Symbol. It helps greatly to understand that the need for Symbol comes from the need to support JavaScript, but that's the problem. It made little sense to me when skimming the documentation what purpose the Symbol class served. And really, its only purpose seems to be to make JavaScript compiling work. Perhaps someday Dart will throw in a little Ruby-like syntactic sugar for defining symbols to make the benefit a little more obvious.

Until then… the tests for another chapter are passing!


Day #877

No comments:

Post a Comment