Search code examples
javajunitjunit5

How can I get similar behavior to @Isolated on a test class, but where the test methods in said class run concurrently with each other?


My case is as follows:

  • I have several test classes to run
  • I want most of them all to run in parallel, and all tests inside to run in parallel too
  • I want one specific test class to run isolated from the others, but tests in them to run in parallel too

Using JUnit5, my junit-platform.properties are:

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

My first approach was to annotate test class I wanted to be separated with @Isolated, but doing that resulted in all tests inside this class to be run sequentially too, which I need to avoid, because those tests are long-running

What could be the other way to achieve this partial sequentiality?


Solution

  • I'm guessing in this class there's a particular set of resources which is being modified in a @BeforeAll and reset in an @AfterAll method in a way which is incompatible with other test classes but self consistent for all methods in the class in question.

    The problem is caused because @Isolated is essentially an alias for @ResourceLock(Resources.GLOBAL), and that annotation is inherited by all the test methods in the class.

    As far as I'm aware it's not possible to acquire a lock in a @BeforeAll method only to release it in an @AfterAll method, at least not with the annotation-based locks controlled by @ResourceLock and co.

    If you're prepared to have the parallelizable test classes share a common base class (or manually add @BeforeAll and @AfterAll methods to them), there is a way to acheive what you want. The solution is based on a ReadWriteLock, and on the fact that parallel read access is allowed, but read-write access is sequentialized.

    First, a utility class to help with sequentialized access:

    public class Sequentializer {
        private static final ReadWriteLock LOCK = new ReentrantReadWriteLock();
    
        public static void beforeParallelClass() {
            LOCK.readLock().lock();
        }
    
        public static void afterParallelClass() {
            LOCK.readLock().unlock();
        }
    
        public static void beforeIsolatedClass() {
            LOCK.writeLock().lock();
        }
    
        public static void afterIsolatedClass() {
            LOCK.writeLock().unlock();
        }
    }
    

    Next, a base class for parallel tests:

    public class ParallelTestBase {
        @BeforeAll
        public static void beforeAll() {
            Sequentializer.beforeParallelClass();
        }
    
        @AfterAll
        public static void afterAll() {
            Sequentializer.afterParallelClass();
        }
    }
    

    Finally, (optional) a base class for test classes which should run separately from all other code:

    public class IsolatedTestBase {
    
        @BeforeAll
        static void setUpBeforeClass() {
            Sequentializer.beforeIsolatedClass();
        }
    
        @AfterAll
        static void tearDownAfterClass() {
            Sequentializer.afterIsolatedClass();
        }
    }
    

    If you're doing complex setup and teardown in the test classes, you may want to call the appropriate Sequentializer methods explicitly rather than relying on inheritance.

    Alternatively, you could build the locking mechanism into whatever resource is being modified by the test classes in question.