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");
}
}

20 comments: