Monday 29 November 2010

Parallel JUnit tests using a custom Concurrent Annotation



concurrent test runs in JUnit 4 are still in experimental phase, a very good alternative to running parallel tests is migrating to TestNG, although if you don't like it or have no possibility to simply migrate to it, you can still use a custom runner by extending the BlockJUnit4ClassRunner class.

first we parametrize the concurrency using an Annotation:


Concurrent.java



@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Concurrent {
    int threadPoolSize() default 3;
    int invocationCount() default 10;
    long timeOut() default 10000;
}



and here comes the actual runner which controls our parallel runs:


ConcurrentRunner.java



public class ConcurrentRunner extends BlockJUnit4ClassRunner {

    public ConcurrentRunner(Class<?> klassthrows InitializationError {
        super(klass);
    }

    @Override
    public void run(final RunNotifier notifier) {

        // run BeforeClass tests
        final List<FrameworkMethod> beforeClassMethods = getTestClass().getAnnotatedMethods(BeforeClass.class);
        for (final FrameworkMethod frameworkMethod : beforeClassMethods) {
            notifier.fireTestStarted(getDescription());
            try {
                methodBlock(frameworkMethod).evaluate();
            }
            catch (Throwable e) {
                Exceptions.throwException(e);
            }
        }

        // run non-concurrent tests
        final List<FrameworkMethod> testMethods = getTestClass().getAnnotatedMethods(Test.class);
        for (final FrameworkMethod frameworkMethod : testMethods) {
            if (frameworkMethod.getAnnotation(Concurrent.class== null) {
                notifier.fireTestRunStarted(getDescription());
                runChild(frameworkMethod, notifier);
            }
        }

        // run concurrent tests, Before and After tests will run automatically before and after each
        // test
        final List<FrameworkMethod> concurrentMethods = getTestClass().getAnnotatedMethods(Concurrent.class);
        for (final FrameworkMethod frameworkMethod : concurrentMethods) {
            final Concurrent concurrent = frameworkMethod.getAnnotation(Concurrent.class);
            if (concurrent != null) {
                final int invocationCount = concurrent.invocationCount();
                final int threadPoolSize = concurrent.threadPoolSize();
                final long timeout = concurrent.timeOut();

                final ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);

                final List<Callable<Object>> calls = new ArrayList<Callable<Object>>();
                for (int i = 0; i < invocationCount; i++)
                    calls.add(new JUnitRunner(frameworkMethod, notifier));

                try {
                    executorService.invokeAll(calls, timeout, TimeUnit.MILLISECONDS);
                }
                catch (InterruptedException e) {
                    Exceptions.throwException(e);
                }

                executorService.shutdown();
            }
        }

        // run AfterClass tests
        final List<FrameworkMethod> afterClassMethods = getTestClass().getAnnotatedMethods(AfterClass.class);
        for (final FrameworkMethod frameworkMethod : afterClassMethods) {
            try {
                methodBlock(frameworkMethod).evaluate();
            }
            catch (Throwable e) {
                Exceptions.throwException(e);
            }
        }
    }

    private class JUnitRunner implements Callable<Object> {

        private final FrameworkMethod method;
        private final RunNotifier notifier;

        private JUnitRunner(final FrameworkMethod method, final RunNotifier notifier) {
            this.method = method;
            this.notifier = notifier;
        }

        @Override
        public Object call() {
            notifier.fireTestRunStarted(getDescription());
            runChild(method, notifier);

            return null;
        }
    }

}



all we need is to test the stuff:


ConcurrentRunnerTest.java



@RunWith(ConcurrentRunner.class)
public class ConcurrentRunnerTest {

    private static Object aGlobalObject = null;
    private static Integer aGlobalInteger = null;

    @BeforeClass
    public static void setUp() throws Exception {
        if (aGlobalObject == null) {
            aGlobalObject = new String("aGlobalObject");
            System.out.println("=========>> Setup test");
        }
    }

    @AfterClass
    public static void coolDown() throws Exception {
        assertNotNull(aGlobalObject);
        assertNotNull(aGlobalInteger);

        assertEquals(aGlobalInteger.intValue()591);

        aGlobalObject = null;
        aGlobalInteger = null;

        System.out.println("=========>> coolDown");
    }

    @BeforeClass
    public static void setUpAgain() throws Exception {
        if (aGlobalInteger == null) {
            aGlobalInteger = new Integer(4177);

            System.out.println("=========>> Setup test 2");
        }
    }

    @Test
    public void testOnce() {
        assertNotNull(aGlobalObject);
        assertNotNull(aGlobalInteger);

        aGlobalInteger = 591;

        System.out.println("===> testOnce");
    }

    @Test
    @Concurrent(threadPoolSize = 4, invocationCount = 10, timeOut = 60000)
    public void testConcurrent() {
        assertNotNull(aGlobalObject);
        assertNotNull(aGlobalInteger);

        System.out.println("===> testConcurrent: " + Thread.currentThread());
    }

    @Test
    public void testOnceAgain() {
        assertNotNull(aGlobalObject);
        assertNotNull(aGlobalInteger);

        System.out.println("===> testOnceAgain");
    }

}




and here is the Output:



=========>> Setup test 2
=========>> Setup test
===> testOnce
===> testOnceAgain
===> testConcurrent: Thread[pool-1-thread-2,5,main]
===> testConcurrent: Thread[pool-1-thread-2,5,main]
===> testConcurrent: Thread[pool-1-thread-1,5,main]
===> testConcurrent: Thread[pool-1-thread-2,5,main]
===> testConcurrent: Thread[pool-1-thread-3,5,main]
===> testConcurrent: Thread[pool-1-thread-3,5,main]
===> testConcurrent: Thread[pool-1-thread-3,5,main]
===> testConcurrent: Thread[pool-1-thread-3,5,main]
===> testConcurrent: Thread[pool-1-thread-2,5,main]
===> testConcurrent: Thread[pool-1-thread-4,5,main]
=========>> coolDown


2 comments:

  1. سلام
    هومن جان خودت نوشتی؟
    میشه توش دست برد؟

    ReplyDelete
  2. آره خودم نوشتم، ولی معلومه که میتونی عوضش کن.

    ReplyDelete