Unit Tests Are Not Enough
The new and improved It's Just a Bunch of Stuff That Happens—which went off the air briefly, as I noted 37 days ago—is back with renewed, focused energy and is as penetratingly insightful as ever.
In a post yesterday, Eric gave a concrete example of the drawback of lack of static typing in Ruby:
The second bug took me far longer to understand and was more insidious. In Ruby you don’t even have to say “return” at the end of a method. Instead, the result of whatever expression happened last is automatically returned. Invisible magic is the quintessential example of a feature that saves the original coder a few extra keystrokes but makes every downstream programmer pay the continuous tax for years to come.
I made the problem worse by adding a simple “if” statement to the end of the method, thinking I could explicitly change a bogus error message to nil. But now the method didn’t even return a string any more…I inadvertently changed the return type from a string to a boolean.
To which I commented without too much thinking:
Had you made a similar return-type-altering change in Java, the compiler would have caught it as an error. In that regard, the static typing works just like unit tests—it prevents you from breaking your code.
This post became the topic of discussion at OCI North this morning. Brian mentioned that he has been bitten by the same problem in a Ruby script he wrote recently. He also pointed out that no amount of unit tests will guarantee that a function will return data of a specific type.
Suppose you want to write a function that takes a string and return a boolean in a dynamically typed language, and you want the function to return true for inputs like "yes", "true" and
def foo(input) {
if input is "yes" return true
if input is "true" return true
if input is "no" return false
if input is "false" return false
}
However, you cannot write a unit test that will make sure this function always returns a boolean. You can write a specific test that prevents the following code from spoiling the function:
if input is "weiqi" return "gao"
But that test cannot prevent the next spoiler, and the next, and the next, ...
In this sense, a statically declared return type of boolean for the function can be thought of as the equivalent of an infinitely many unit tests.
The conclusion:
Re: Unit Tests Are Not Enough
However, you cannot write a unit test that will make sure this function always returns a boolean.
I think this is a true statement with regards to Ruby and perhaps to dynamically-typed languages in general, that unless you write an infinitely long unit test in which you attempt to pass all possible values you cannot guarantee that a method will *always* return a boolean. However, your code example doesn't contain an else condition so (if it were written in Ruby) the method would either return boolean in some cases or nil in all others. I take your point that a language that does not force you to return values of a particular type would allow you to return objects of varying types under different conditions or not return anything at all (like returning null in Java). From your example, the equivalent in Java could be:
public Boolean foo(Object input) {
Boolean result = null;
if "yes".equals(input) result = Boolean.TRUE;
if "true".equals(input) result = Boolean.TRUE;
if "no".equals(input) result = Boolean.FALSE;
if "false".equals(input) result = Boolean.FALSE;
return result;
}
Since in Ruby everything is an object, I think the above Java example is a valid equivalent to your original code example; the above will always return either a Boolean reference or null. IMHO this is more an example of incomplete programming than of the strength or danger of the language. Just like Spiderman says, 'with great power comes great responsibility.'
Re: Unit Tests Are Not Enough
def foo(input)
["yes", "true"] & ([] << input) != nil
end
then you won't have any corner cases to worry about, and it will always return true or false i don't see that this makes a strong case for static typing.
Re: Unit Tests Are Not Enough
Isn't the whole idea of static typing or unit testing or whatever other post-coding verification precisely to catch the implementation that we _thought_ was decent?
It may be easy to come up with an obviously decent implementation in _this_ particular case, but IMHO that's really missing the point here.
People are often so easily thrilled by the convenience brought forth by dynamic languages, without realizing that it doesn't come for free.