Wednesday, November 4, 2009

Writing your own JUnit extensions using @Rule

This article follows the previous one concerning the really useful and undocumented features of Junit. One that is really interesting is @Rule.
This annotation allows you to annotate a public field in your test class, which is of type MethodRule. This binding will intercept test method calls like an AOP framework would do and redefine the execution, skip it, or do anything else.
In example, suppose you want to run some concurrency test: you may need to execute your test method on 15 threads each starting at the same time, and then wait for all threads to finish. All this plumbing can now be resumed by one annotation:

@Test
@Concurrent(15)
public void myTestMethod() throws InterruptedException {
System.out.println("Thread " + Thread.currentThread().getName() + " started !");
int n = new Random().nextInt(5000);
System.out.println("Thread " + Thread.currentThread().getName() + " wait " + n + "ms");
Thread.sleep(n);
System.out.println("Thread " + Thread.currentThread().getName() + " finished");

But who manages this annotation ? It will be our MethodRule, and it is used like this:

public final class ConcurrentTest {

@Rule
public ConcurrentRule concurrentRule = new ConcurrentRule();

@Test
@Concurrent(15)
public void myTestMethod() throws InterruptedException {
System.out.println("Thread " + Thread.currentThread().getName() + " started !");
int n = new Random().nextInt(5000);
System.out.println("Thread " + Thread.currentThread().getName() + " wait " + n + "ms");
Thread.sleep(n);
System.out.println("Thread " + Thread.currentThread().getName() + " finished");
}
}

MethodRule are simplier instances that will intercept all test calls. They are mere instance variable that can benefit of parametrization. In example, we may want in a test class to have a ConcurrentRule with a timeout of 10 seconds, and 2 seconds in another class.
In our case, the ConcurrentRule I made simply check if the test method intercepted has a Concurrent annotation. If yes, it will spawn the number of thread requested and launch the test method for each thread.
Here is the annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Concurrent {
int value() default 10;
}

And the MethodRule implementation:

import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import java.util.concurrent.CountDownLatch;

public final class ConcurrentRule implements MethodRule {
@Override
public Statement apply(Statement statement, final FrameworkMethod frameworkMethod, final Object o) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Concurrent concurrent = frameworkMethod.getAnnotation(Concurrent.class);
if (concurrent == null)
statement.evaluate();
else {
final String name = frameworkMethod.getName();
final Thread[] threads = new Thread[concurrent.value()];
final CountDownLatch go = new CountDownLatch(1);
final CountDownLatch finished = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
try {
go.await();
statement.evaluate();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException)
throw (RuntimeException) throwable;
if (throwable instanceof Error)
throw (Error) throwable;
RuntimeException r = new RuntimeException(throwable.getMessage(), throwable);
r.setStackTrace(throwable.getStackTrace());
throw r;
} finally {
finished.countDown();
}
}
}, name + "-Thread-" + i);
threads[i].start();
}
go.countDown();
finished.await();
}
}
};
}
}

All the code is available in Mycila sandbox here.

Update: 2009-12-23
A suggestion in the comments below was to be able to retrieve the exception if the test fail and rethrow it just after in the main thread. This can be done easily using Java Concurrent API, with FutureTask or CompletionService like this:

import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;

import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;

/**
* @author Mathieu Carbou (mathieu.carbou@gmail.com)
*/
public final class ConcurrentThrowingRule implements MethodRule {
@Override
public Statement apply(final Statement statement, final FrameworkMethod frameworkMethod, final Object o) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Concurrent concurrent = frameworkMethod.getAnnotation(Concurrent.class);
if (concurrent == null)
statement.evaluate();
else {
// create an executor which cimply spawns threads to execute runnables
Executor executor = new Executor() {
final String name = frameworkMethod.getName();
int count = 0;

@Override
public void execute(Runnable command) {
new Thread(command, name + "-Thread-" + count++).start();
}
};
// create a completion service to get jobs in the order they finish, to be able
// to cancel remaining jobs as fast as possible if an exception occurs
CompletionService<Void> completionService = new ExecutorCompletionService<Void>(executor);
// latch used to pause all threads and start all of them (nearly) at the same time
final CountDownLatch go = new CountDownLatch(1);
// create the tasks
for (int i = 0; i < concurrent.value(); i++) {
completionService.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
try {
go.await();
statement.evaluate();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Throwable throwable) {
if (throwable instanceof Exception)
throw (Exception) throwable;
if (throwable instanceof Error)
throw (Error) throwable;
// case of exceptions directly subclassing Throwable
// (should not occur - bad programming)
RuntimeException r = new RuntimeException(throwable.getMessage(), throwable);
r.setStackTrace(throwable.getStackTrace());
throw r;
}
return null;
}
});
}
go.countDown();
Throwable throwable = null;
for (int i = 0; i < concurrent.value(); i++) {
try {
completionService.take().get();
} catch (ExecutionException e) {
// only keep the first exception, but wait for all threads to finish
if(throwable == null)
throwable = e.getCause();
}
}
if(throwable != null) throw throwable;
}
}
};
}
}

Then you can use the expected attribute in your Junit test:

public final class ConcurrentThrowingTest {

@Rule
public ConcurrentThrowingRule concurrentRule = new ConcurrentThrowingRule();

@Test(expected = NumberFormatException.class)
@Concurrent(10)
public void myTestMethod_failing() throws InterruptedException {
System.out.println("Thread " + Thread.currentThread().getName() + " started !");
int n = new Random().nextInt(5000);
System.out.println("Thread " + Thread.currentThread().getName() + " wait " + n + "ms");
Thread.sleep(n);
Integer.parseInt("blabla");
}
}

22 comments:

  1. Hi Mathieu, good to know. Thanks.

    I started to notice your blog a while ago.

    ReplyDelete
  2. Few times you mention "ManageRule" instead of MethodRule.
    Cheers

    ReplyDelete
  3. If go.await() is interrupted, shouldn't you either throw the exception or at least not execute the actual test?

    ReplyDelete
  4. Yes absolutely ! I've fixed the code.
    Thank you !

    ReplyDelete
  5. Very interesting and useful blog entry!
    Thank you.

    ReplyDelete
  6. A little question, shouldn't you call
    statement.evaluate() (statement is the param)
    instead of
    frameworkMethod.invokeExplosively(o)?

    Probably I'm wrong but it seemed to me that Kent Beck in his blog entry on @Interceptor says that the target and the method parameters are there for additional validation/logging purpose?

    ReplyDelete
  7. Yes you're right. In case the Statement is not an instance of InvokeMethod, I was skipping its call... I am fixing it now.

    ReplyDelete
  8. Great post.

    How'd you like

    http://code.google.com/p/tempus-fugit/source/browse/trunk/tempus-fugit/src/main/java/com/google/code/tempusfugit/concurrency/RunConcurrently.java

    as an alternative? I wanted to try and get any failure thrown on a non-test thread to still fail the test. Its a bit rough and ready at the moment but I had fun coding it!

    cheers

    ReplyDelete
  9. Yes it's a good idea ! Instead of playing with threads, it can be simply and much more reliable to use instead FutureTask or CompletionService to handle execution and get the results or exceptions thrown.

    I've updated this post to give an example.

    Thanks !

    ReplyDelete
  10. Hi Mathiu,
    glad to see thar we have arrived to a similar solution(currently I must yet refactorize it and writing some unit test and checking...this version was temporary): http://pastie.org/753201

    ReplyDelete
  11. Great, I like the completion service version. It was bugging me that in my version it'd wouldn't return fast (as in the first thread in the list must finish before things continue, when the second in the list may have already finished).

    ReplyDelete
  12. Did you run the test red above? It seems if you comment out the line

    if(throwable != null) throw throwable;

    it still runs green, is that a false positive?

    ReplyDelete
  13. No, this is because when you use @Test(expected = ...), Junit create a statement surrounding the statement executing the test method. It means that when we run statement.execute(), the ExpectedException statement runs and call the InvokeMethod statement. If the latter fails,the ExpectedException statement will check the exception and match it against the expected.

    The code
    if(throwable != null) throw throwable;
    is only required for a failing test throwing an exception.

    ReplyDelete
  14. ah, I spotted the behavior in ExpectException, I can't figure out tthough that if an exception is thrown from another thread, it gets swallowed somewhere as without the re-throw line above you get a green bar when the exception was thrown, caught (on another thread) so never thrown on the test thread. I figure the test should go red in this case because its expecting a X exception which it doesn't get (as its thrown on another thread). Unless its the ExpectException statement that is still on the main thread...

    At least that's what's confusing me at the moment. I'm not explaining it very well, but its got me all in a tiz waz!

    ReplyDelete
  15. Junit's behavior is good: what you are running in parallel is a statement, which contains the whole steps of the Junit test : execution + post-execution tests like the ExpectedException if you define one.

    If you expect an exception to happen, this is logical that the test doesn't fail if this exception is really thrown.

    You can test by throwing a different exception, or by removing expected=... in @Test. You'll see that the test fails.

    ReplyDelete
  16. Its not that, if you remove the rethrow line but still throw an exception in your (now parallel) test, with an expected class for Foo but you actually throw a Bar. You get a pass, it doesn't say expected but was.

    With the throw back in, its all fine. What I was getting at here is how to write a unit test to prove that the rethrow has been put in the code. At the moment it looks like a false positive.

    See you amended code to demonstrate the point at http://pastie.org/760070 (surely, this test should fail?)

    ReplyDelete
  17. If you debug and learn how Junit is built, perhaps you'll understand much more why

    if(throwable != null) throw throwable;

    is required.

    A Junit test method is executed as a Statement. By default, implemented as an InvokeMethod. If you put in your test @Test(expected=Foo.class), Junit wraps the InvokeMethod around an ExpectException.

    So what is executed in parallel is NOT THE TEST METHOD but THE WHOLE TEST INCLUDING THE EXCEPTION ASSERTION.

    What ExpectException statement does:
    1. run the surrounded statement
    2. if it fails, check that the expected exception is the one we got
    2.1 IF YES: all is good => don't rethrow any excception since the test passed
    2.2 IF NO: throw an assertion error saying that we expected Foo.class but we got anothe exception.

    In your case, you run in parallel a test throwing an AssertionError, but you expect an IOException. So the Junit Statement throws an AssertionError saying this.

    Since the test is run in parallel, this laster exception is trapped and rethrown by

    if(throwable != null) throw throwable;

    => @Test(expecte=Foo.class) does not assert against the exception thrown by

    if(throwable != null) throw throwable;

    but assert against the exception thrown by the test method.

    That's why

    if(throwable != null) throw throwable;

    is required. And if you look a little deeper with a debugger, you should see that 'throwable' has the value:

    Unexpected exception, expected but was

    This is the exception Junit created in the ExpectException statement, executed 10 times concurrently.

    ReplyDelete
  18. Just what I was looking for. I wanted to intercept the test method name and JUnit implements already this functionnality with Rule.

    public class NameRuleTest {
    @Rule public TestName name = new TestName();

    @Test public void testA() {
    assertEquals("testA", name.getMethodName());
    }

    @Test public void testB() {
    assertEquals("testB", name.getMethodName());
    }
    }


    Thanks. This blog entry is realy useful.

    ReplyDelete
  19. Hello Mathieu!

    Great post.
    I have one problem though as soon I happen to extend TestCase from my class the @Rule annotation stop working. Are you aware of this problem and can explain how to solve it.

    ReplyDelete
  20. Hi here.

    Thank for your excellent work.

    Still, i'm experiencing a problem with your solution :

    My SUT is an object instance that will get shared between a great many deals of threads, hence the usage of your code. The object is quite complicated to build (it's a composite, with builders and so on). Therefore i've put the burden of building it in a prepare() method, which is as @Before annotated.

    The prepare() method builds the object and saves it as a field in the test class.

    What's annoying me, is that the prepare() method is also concurrently called by the rule. Thus is the object more or less never shared between threads. Well, that might happens, but it depends on race conditions of the test class !

    Is there a (clean) way to chance the code of the Rule class, in a way where only the actual test method is concurrently called?

    Also, i'd like to recreate anew the instance between every test case. So using a static instance and a @BeforeClass prepare() method does not really suit me.

    Huge thanks again for your work !

    Antoine

    ReplyDelete