First published 2009. The coding session referenced still ranks as one of the highlights of my career. A “tidy, tidy, tidy, oh wow look at this cool abstraction that fell out” moment.
I’m almost done republishing pieces from my old web site & moving into the 2010s. That’ll be fun.
Finally, I’m publishing draft chapters of the next software design book, "Tidy Together?, to paying subscribers. If you want to be an early reader (& commenter), here’s your chance.
<<<update>>> We have changed the name of the abstraction to “Rule” from “Interceptor”. The code and documentation read much better. <<<>>>
It must have been the little one-week-old on his lap. Saff and I had a remarkable programming session Tuesday night. Maybe once every five years unsuspectedly powerful abstractions drop out of a program with no apparent effort. As it turns out we’ve been preparing for this particular magical moment for a while, but it was nice to have it happen finally. And I really think the baby did the trick.
Meta-testing
JUnit is good for expressing tests as little bits of logic. In JUnit 3 you could also manipulate the test running process itself in various ways. One of the prices of the simplicity of JUnit 4 was the loss of this “meta-testing”. It doesn’t affect simple tests, but for more powerful tests it can be constraining. The object framework style of JUnit 3 lent itself to extension by default. The DSL style of JUnit 4 doesn’t. Last night we brought back meta-testing, but much cleaner and simpler than before.
For example, suppose you want to write to a log every time a test fails. As of late last night, when you annotate a field with @Interceptor, the object in that field is notified before the test is run. Thus, you can write:
@Interceptor
public StatementInterceptor logger= new LoggingInterceptor();
With the @Interceptor declaration, logger is called before the test is run. For the moment interceptors aren’t part of the standard test runner, so you need to run tests containing interceptors with a special runner, like this (we expect to make interceptors part of the default behavior in the 4.7 release):
@RunWith(Interceptors.class)
public class MyLoggingTest {
@Interceptor
public StatementInterceptor logger= new LoggingInterceptor();
}
We created a simple general interceptor, TestWatchman, that contains hook methods called when a test succeeds or fails. Extending TestWatchman lets you log errors:
public class LoggingInterceptor
extends TestWatchman {
public void failed(Throwable e, FrameworkMethod method) {
log(method.toString());
}
}
So, the simple way to introduce an interceptor is to declare a field as a StatementInterceptor, annotate it with @Interceptor, and initialize it with an instance of your own subclass of TestWatchman in which you override succeeded() or failed().
Writing Interceptors
That’s just the beginning of what you can do with interceptors. You can also define your own interceptors from scratch. Two More Implementation Patterns describes the implementation of test runners in some detail. Here is a summary. Each test is run by a chain of Statement objects. Each Statement takes care of one aspect of test running, like running @Before methods or causing a test to fail if a timeout is exceeded. The whole “block” of “statements” that runs an ordinary test looks like this:
RunAfters -> RunBefores -> InvokeMethod
Interceptors are inserted into this block. For the LoggingInterceptor example above, the block looks like this:
RunAfters -> LoggingInterceptor -> RunBefores -> InvokeMethod
(Actually, now that I look at it, RunAfters and RunBefores should only appear if there are actually @Before/@After methods, but that’s an optimization for another day.)
You can’t control where your interceptors will be inserted into the block. You just know they’ll be in there somewhere. As mentioned above, the object occupying the @Interceptor field must implement StatementInterceptor. StatementInterceptor declares a factory method intercept() that produces a Statement of your own devising given the next Statement in the block and a FrameworkMethod. FrameworkMethod is JUnit’s version of java.lang.reflect.Method, wrapping a Method and providing additional behavior to validate methods for use as tests.
public class MyInterceptor
implements StatementInterceptor {
public Statement intercept(
final Statement next,
final FrameworkMethod method) {
return new Statement() {
public void evaluate() throws Throwable {
// here is where you put the behavior you want
next.evaluate();
// or here
}
}
}
}
Note that you should generally not capture the Throwable generated by the recursive call to evaluate(), although you may want to catch it and rethrow it after processing it in some way.
Conclusion
We have only begun exploring what is possible with interceptors. Many of the extensions people have requested that would have required an entire custom test runner can now be implemented with interceptors. Since interceptors are just fields in the test object, it should also be possible to store information during test running and process that information after the test completes. And I’m sure Saff will have some excellent examples, just as soon as he gets that diaper cleaned up.
If you want to try interceptors, you need to check JUnit out of the master repository on GitHub: git://github.com/KentBeck/junit.git [ed: now https://github.com/junit-team/]. If there is sufficient interest we will also upload a snapshot to SourceForge [ed: lol].
That SourceForge lol made me laugh 😀 How complicated some things have been in the past, how simple they have become. As with JUnit Rules…
Is this similar to Aspect Oriented Programming of the 90's (described as cross-cutting concerns)?