I've always been slightly skeptical of claims that thorough unit tests in dynamically typed languages are a replacement for the guarantees that a static type checker offers. In the simple case it seems to work fine, but as examples get more advanced I start to feel uneasy. For instance, consider the following Groovy test method that produces an object representing an email message:
def server = new MailServer()
def message = server.getNewestMessage()
assert message.to == ["hamletdrc@gmail.com"]
assert message.from == "hamletdrc@gmail.com"
assert message.subject == "note to self"
assert message.body == "sample message"
This unit test seems to thoroughly exercise the message object, and I doubt many people would feel the need to test message beyond this. But what if the message object is defined with a truncate method and not just fields:class Message {
def to = []
def from
def subject
def body
Message(to, from, subject, body) {
this.to.addAll(to)
this.from = from
this.subject = subject
this.body = body
}
def truncate = {
new Message(to, from, subject, body.substring(0, 50))
}
}
Is the original test enough? What if the message object returned from the server doesn't have a truncate method? What if, instead of an actual Message object, one of the methods returned a closure map:def getNewestMessage = {
def message = inbox.get(0)
return [
to: message.to,
from: message.from,
subject: message.subject,
body: message.body
]
}
The unit test will still pass, but any calling code in production receives a nice runtime exception when attempting to call truncate. There are no guarantees on the contract of types in dynamic languages, but this is exactly the sort of thing a static type checker would enforce. It has always seemed to me that if unit tests are indeed a replacement for a type checker then those unit tests should guarantee that the contract is followed. It would be a monumental task to ensure in tests that all expected methods exist on an object. This is what makes me feel uneasy about claims that unit tests are a replacement for a type checker.Enter ECMAScript 4 and the like operator. ECMAScript 4 contains a like operator that is a sort of weak instanceof. I stumbled upon it in the paper Evolutionary Programming and Gradual Typing in ECMAScript 4. The like operator is an assertion that a certain object looks a certain way at a certain time. For instance, an arbitrary object is "like" a java.lang.String if its set of public methods and fields contains the set of methods and fields on String. It is not an "is" comparison, rather it is just checking to make sure an object adheres to the declared contract of another. The "point in time" distinction is important, because the object could have methods added or removed later.
This intrigued me. With the inclusion of a like operator in unit tests, you could claim that those tests are a replacement for the type checker. Imagine the test rewritten:def server = new MailServer()
def message = server.getNewestMessage()
...
assert message like Message
I was interested enough to actually try to write a like operator, (short sighted enough, as it turned out). Or, since I can't really write an operator in Groovy, I'd write an assertTypeLike assertion method to use in my Groovy tests.It started out simple enough... Groovy 1.5 includes a new class called Inspector. This allows you to get real and virtual methods on objects. Create a list of public signatures on the test object, create the same list from your control, compare, and viola, a type like comparison.
Except that this turned out to be really, really difficult. There are a ton of edge cases. A HashMap of closures can be treated as an object. Methods on an object can be defined as closures. One method interfaces can be implemented as closures. The list goes on. But the killer is that my solution totally ignored metaprogramming with either invokeMethod or methodMissing.Had I thought a little harder, I would have realized that a like operator will never work in a language like Groovy. Since any method call can be intercepted and responded to, even those that don't exist, it is not possible to analyze the structure of a class and determine if it fulfills some contract (or even if it has a contract for that matter!).
The like operator is a cool idea. I feel Adobe has been trying to placate both sides (dynamic and static) by taking a best of both worlds approach to ActionScript. Promoting a type system that can be gradually enforced stricter and stricter as a project grows is one example of this. As someone coming from a C++/Java background, this appeals to me! But would I give up metaprogramming for this? Not a chance!In the end I have to ask myself, what am I trying to prove? Am I trying to prove that my software works correctly for my customer? I am, and unit tests are essential to this, not a type checker. Am I trying to prove that my objects won't blow up if called in a certain way that I don't expect? The like operator can help me here, but I DON'T have this problem. I can't point to a single production issue that the like operator would have saved me from. So why in the world does my mind keep pushing me to test types in this way? Perhaps this is my Java mind clinging to the comfortable. If you have better ideas then let me know! In the meantime, I'm going to continue coding without the safety net.
As a parting comment, page 2 of the paper contains the funniest definition of duck typing I've seen yet:This kind of type discipline is often known as "duck typing", on the principle that if something walks like a duck and talks like a duck, it is a duck. (This should not be confused with an older and now discredited type discipline, which says that if a woman floats like a duck then she's a witch. ES4 has no objects that flot like ducks, but it does have floats that bewitch - decimals.)Awesome. My groovy test case for assertTypeLike can be found here.
No comments:
Post a Comment