Tuesday, January 8, 2013

Fun with noSuchMethod in Dart Subclasses

‹prev | My Chain | next›

I find that one of the things I like most while writing Dart unit tests for the forthcoming update to Dart for Hipsters is asserting things that will not work. It is a good way to verify that assertions that I make in the book are actually valid. More fun is when an old assertion proves to be no longer valid.

While talking about varying runtime behavior in Dart, I make the assertion that defining noSuchMethod() in a subclass prevents Dart from calling noSuchMethod() in the superclass. For all I know, that could have changed since I wrote that paragraph. Even if it has not changed, it could very well change in the future so I really need a test to verify that assertion.

In my test, I define a superclass A whose noSuchMethod() throws an exception and a subclass B whose noSuchMethod() does nothing:
import 'package:unittest/unittest.dart';

class A {
  noSuchMethod(args) {
    throw new NoSuchMethodError();
  }
}

class B extends A {
  noSuchMethod(args) {}
}

run() {
  group("[noSuchMethod]", (){
    test('cannot access noSuchMethod in superclass when defined in subclass', (){
      var b = new B();
      expect(()=> b.foo(), returnsNormally);
    });
  });
}
In the test, I have created an instance of B and invoked a non-existent method foo() that should find its way into noSuchMethod(). And indeed, it does hit noSuchMethod() because the expectation in that test that invoking the foo() method returns normally proves to be correct:
➜  varying_the_behavior git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
PASS: [noSuchMethod] cannot access noSuchMethod in superclass when defined in subclass

All 1 tests passed.
If noSuchMethod() in the subclass had not been invoked, then Dart would have thrown an error. If the noSuchMethod() method had been invoked in the superclass, the superclass would have thrown an error. Since the method returns normally, I am hitting noSuchMethod() in the subclass as desired.

So the text is correct and I can move on right? Maybe, but then again, maybe not.

The args that are being passed into noSuchMethod() are not a list of parameters—they are an InvocationMirror object. Perhaps there is a way to invoke the superclass's noSuchMethod() with this InvocationMirror doohicky.

The only aspect that seems to have potential in InvocationMirror is the invokeOn() method. But I cannot actually figure out what to invoke invokeOn()... on. If I try it on this, I get stack overflow:
class C extends A {
  noSuchMethod(args) {
    args.invokeOn(this);
  }
}
// ...
    test('can access noSuchMethod in superclass when defined in subclass', (){
      var c = new C();
      expect(()=> c.foo(), throwsNoSuchMethodError);
    });
//  ...
This results in the aforementioned stack overflow:
➜  varying_the_behavior git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
FAIL: [noSuchMethod] cannot access noSuchMethod in superclass when defined in subclass
  Expected: throws an exception which matches NoSuchMethodError
       but:  exception <Stack Overflow> does not match NoSuchMethodError.
And I cannot invokeOn() on super:
class C extends A {
  noSuchMethod(args) {
    args.invokeOn(super);
  }
}
Dart will not even compile that:
➜  varying_the_behavior git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
'file:///Code/varying_the_behavior/test/superclass_no_such_method.dart': Error: line 18 pos 24: illegal use of 'super'
    args.invokeOn(super);
                       ^
And then it occurs to me: I want the noSuchMethod() method in the superclass to be invoked with the InvocationMirror that I already have. Perhaps I can call super.noSuchMethod() and supply my args InvocationMirror to it:
class C extends A {
  noSuchMethod(args) {
    return super.noSuchMethod(args);
  }
}
It seems that I can do that as my test now passes:
➜  varying_the_behavior git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
PASS: [noSuchMethod] can access noSuchMethod in superclass when defined in subclass

All 1 tests passed.
This even works with parameters. I update the super class to extract sum parameters passed to the non-existent "bar" method then multiply the result by 2:
class A {
  noSuchMethod(args) {
    if (args.isMethod && args.memberName == "bar") {
      return 2 * args.
        positionalArguments.
        reduce(0, (prev, element) => prev + element);
    }

    throw new NoSuchMethodError();
  }
}
I then write a test that invokes the bar() method on the subclass:
    test('can pass parameters to noSuchMethod in superclass', (){
      var c = new C();
      expect(c.bar(2,2), equals(2*(2+2)));
    });
And it passes:
➜  varying_the_behavior git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
PASS: [noSuchMethod] can access noSuchMethod in superclass when defined in subclass
PASS: [noSuchMethod] can pass parameters to noSuchMethod in superclass

All 2 tests passed.
Now if you'll excuse me, I have a chapter to rewrite...


Day #624

No comments:

Post a Comment