Wednesday, November 21, 2007

Testability vs. Encapsulation

I'm always on the side of Testability, but I'd like to hear some other peoples opinions on the subject. I think (in most situations) its totally OK to relax encapsulation in favor of testability, but some people at my work do not. Let me give a rather long example of something I came across that bothered me a bit.

I was trying to test a class - well call it G, with a Logger - LOG , which was declared like this:

static final Logger LOG = Logger.instance();

Later on in G, I found a method shutdown, which called LOG.logAndExit() and logAndExit was final and called System.exit().

G:
public void shutdown(){ LOG.logAndExit( "shutting down" ); }


Logger:
public final void logAndExit( String message ){

System.out.println(message);
System.exit(0);
}

If my tests wanted to test the shutdown method on G, the entire JVM would shutdown which simply shutdown my tests. Brilliant. I had to find some way to Mock or override the LOG variable. But there were several problems with that.

  1. It was private
  2. It was final
  3. It was static
  4. The logAndExit method was final

All of these things make difficult testing. I tried to get around the "private" by writing my PrivateFieldHelper Class. But, as it turns out, you cannot change the accessibility of static final variables at Runtime. It only works for instance fields of all types, and non final static variables. So I was stuck with the LOG object that I had.

Unless of course I relaxed encapsulation and removed the final from LOG. Then I could use my PrivateFieldHelper to set it to a new Logger of some kind. BUT...I still had a problem.
The logAndExit method was final. So even if I extended our Logger class and tried to override logAndExit so that it wouldn't call System.exit(), I still could not do so. Even when I created a Mock Logger object using JMock I had the same problem. It appears that even mock objects can't override final methods.

So once again I decided it was best to relax encapsulation. I removed the final keyword from Logger logAndExit, and created a new class - SafeLog - that overrode the logAndExit method. I used my PrivateFieldHelper to set the (now only static private) LOG field on G, and I was able to safely call the shutdown method from my test code.

What a pain.


I understand that you can go way too far on this. Some people say you should never relax encapsulation for testability, because in doing so you relax intent and readability which later creates more of a maintenance problem. In some ways I do agree with this. On public API's and Libraries you most certainly will have higher maintenance costs. But, I do think removing a final here and a final there is ok, especially if its documented. I also think removing private in favor of default (package private) is ok.

What do you think? Does anyone know of any good articles or books explaining the trade-offs?

God this post is about to get long....

Some say you shouldn't relax private for package private, and all testing should be done through public methods. This is another one I just don't agree with. Lets say you have a reasonably complicated class that only exposes one public method. People reading the class later on might not know what inputs are valid for and what outputs are expected for each of the private methods in the class. You certainly can, and should get 100% code coverage through testing public methods, but it still might not be immediately obvious to someone reading the code later on what those private methods are doing.

Testing private (or package private) methods extensively should make it immediately obvious. Once again, What do you think? Does anyone know of any good articles or books explaining the trade-offs?

I think there can and should be things built into the languages themselves to expose hidden members to testing. Something like a test keyword. Or like JSR 294, superpackages, which define what classes in a package are accessible, and to whom. If it things like this were built right into the language, exposing members to testing, then we wouldn't even be having these debates.

How would it work? Maybe a lot like Generics. All the type information is removed at compile time, and so could any test availability information. You could turn this off. You could say, produce a jar for testing, and produce a jar for delivery. Its not that hard. Just ideas...You have any?

2 comments:

  1. C++ has this - the friend keyword. The Java people obviously thought it added too much complexity.

    I have no problems with weakening access specifiers for testing in Java. That's mostly because I think that the whole Java access specifier system is shit, though, together with the one class=one file=one namespace thing. A lot of languages let you declare a module namespace with explicit exports, and then let any code put itself in that namespace or import things that aren't exported. Your test code can do this, without worrying about coupling across the interface definition, because it's yours.

    Even a jvm language like Scala has companion objects that can access private things in its associated object.

    Since you're so into testing you might be interested in Lazy Smallcheck, which exploits partial application of functions to quickly test properties without having to test all the possible inputs. There's lot of truly mindbending things that people do with the Haskell type system that could be exploited for increased testability , and of course the type system itself, being something better than the dog's breakfast Java gives you, gives you a lot of that for free.

    ReplyDelete
  2. Sure seems like I have a lot of reading to do.

    Do you think some of the type systems in other languages were built with testability in mind, or do you think it was that testing came for free given a very solid type system? Or maybe a combination of both?

    How much do language designers consider testability? I know you've said before that the only reason that Java has so much testing is that the language is poor to begin with (maybe you didn't say that, but I'm pretty sure you did), and I don't really agree with that. If I were to start writing code in any other language, I'd still want to start with unit tests.

    In fact, if I were to start writing code in a new (or new to me) language, I'd probably want to rely on my unit tests even more than ever.

    And I'm sorry if you didn't say that, but if you did, can you give some examples of how you could use considerably fewer tests in another language, and explain why?

    ReplyDelete