Home Junit 5 Junit 5 parallel tests execution and @ResourceLock examples

Junit 5 parallel tests execution and @ResourceLock examples

By default, JUnit Jupiter tests are run sequentially in a single thread. In this article you will see examples of several configurations for Junit 5 parallel tests execution and examples for how to get synchronized access with @ResourceLock to get shared resource.

Parallel tests execution available since Junit 5.3 and still is an experimental feature. This is really an important feature to reduce execution time for running large number of test sets, particularly to run web automation test cases. Junit 5 came with very simple and powerful configurations to control concurrency for test classes and test methods. @ResourceLock also nice feature for synchronizing access to the shared resources. Let’s have a look into configurations with examples.

Technologies used in following examples :

  1. Junit 5.5.2
  2. Maven 3
  3. Java 8
  4. Spring Tool Suite 3.9.8

Example Project Structure :

junit 5 parallel tests execution and synchronization with @ResourceLock example

1. Configuration to enable parallel execution

We can configure parallel tests execution for Junit 5 tests by configuring junit.jupiter.execution.parallel.enabled configuration parameter value to true. we can do it in several ways like, by providing parameters to maven surefire plugin or by providing as System properties to JVM, the simplest way is include parameter junit.jupiter.execution.parallel.enabled=true in junit-platform.properties .

1.1. Configuration to execute all tests in parallel

1.1.1. Set the following configuration in junit-platform.properties that exist in src/test/resources. Following configuration enables to execute all tests in parallel.

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

1.1.2. To demonstrate parallel execution for tests classes and test methods I have created 3 groups of test cases.

Test Group1 :

@Tag("group1")
public class Parallel1_Test {
	
	@Test
	void test1A() {
                //.currentThread.getName() -just to display current running thread name
		System.out.println(Thread.currentThread().getName()+" => test1A");
	}
	
	@Test
	void test1B() {
		System.out.println(Thread.currentThread().getName()+" => test1B");
	}

	@Test
	void testC() {
		System.out.println(Thread.currentThread().getName()+" => test1C");
	}	
}

Test Group2 :

@Tag("group2")
public class Parallel2_Test {
	
	@Test
	void test2A() {
		System.out.println(Thread.currentThread().getName()+" => test2A");
	}
	
	@Test
	void test2B() {
		System.out.println(Thread.currentThread().getName()+" => test2B");
	}

	@Test
	void test2C() {
		System.out.println(Thread.currentThread().getName()+" => test2C");
	}
}

Test Group3:

@Tag("group3")
public class Parallel3_Test {
	
	@Test
	void test3A() {
		System.out.println(Thread.currentThread().getName()+" => test3A");
	}
	
	@Test
	void test3B() {
		System.out.println(Thread.currentThread().getName()+" => test3B");
	}

	@Test
	void test3C() {
		System.out.println(Thread.currentThread().getName()+" => test3C");
	}
}

1.1.3. Run the tests :

$ mvn clean test

Following is the partial output, you may get different output, but observe the thread names in console, You will notice that all the test classes and test methods executed in parallel, tests are started by different threads. If you disable parallel tests configuration and run, you will notice that all the tests run by same thread.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.javabydeveloper.util.Parallel1_Test
[INFO] Running com.javabydeveloper.util.Parallel3_Test
[INFO] Running com.javabydeveloper.util.Parallel2_Test
ForkJoinPool-1-worker-5 => test1A
ForkJoinPool-1-worker-3 => test3C
ForkJoinPool-1-worker-7 => test2C
ForkJoinPool-1-worker-1 => test1C
ForkJoinPool-1-worker-1 => test1B
ForkJoinPool-1-worker-3 => test3B
ForkJoinPool-1-worker-3 => test3A
ForkJoinPool-1-worker-7 => test2B
ForkJoinPool-1-worker-5 => test2A
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 s - in com.javabydeveloper.util.Parallel3_Test
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.118 s - in com.javabydeveloper.util.Parallel1_Test
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.136 s - in com.javabydeveloper.util.Parallel2_Test
[INFO]
[INFO] Results:
[INFO]
[WARNING] Tests run: 11, Failures: 0, Errors: 0, Skipped: 2
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

1.2. Configuration to execute top-level classes in parallel but methods in same thread

1.2.1. Following configuration enables you to run top-level classes in parallel but methods in same thread.

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

1.2.2. Run the tests :

$ mvn clean test

You will notice from the out put that, each group of tests started by one different thread and each group test methods started by same thread.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.javabydeveloper.util.Parallel1_Test
[INFO] Running com.javabydeveloper.util.Parallel3_Test
[INFO] Running com.javabydeveloper.util.Parallel2_Test
ForkJoinPool-1-worker-7 => test2A
ForkJoinPool-1-worker-1 => test1A
ForkJoinPool-1-worker-3 => test3A
ForkJoinPool-1-worker-7 => test2B
ForkJoinPool-1-worker-7 => test2C
ForkJoinPool-1-worker-3 => test3B
ForkJoinPool-1-worker-1 => test1B
ForkJoinPool-1-worker-1 => test1C
ForkJoinPool-1-worker-3 => test3C
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.082 s - in com.javabydeveloper.util.Parallel2_Test
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.105 s - in com.javabydeveloper.util.Parallel1_Test
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.122 s - in com.javabydeveloper.util.Parallel3_Test
[INFO]
[INFO] Results:
[INFO]
[WARNING] Tests run: 11, Failures: 0, Errors: 0, Skipped: 2
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

1.3. Configuration to execute top-level classes in sequentially but their methods in parallel

1.3.1. Following configuration enables you to run top-level classes in sequentially but their methods in parallel.

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

1.3.2. Run the tests :

$ mvn clean test

You will notice from the out put that, each test class started sequentially and test methods are executed in parallel by different threads.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.javabydeveloper.util.Parallel1_Test
ForkJoinPool-1-worker-7 => test1B
ForkJoinPool-1-worker-3 => test1C
ForkJoinPool-1-worker-5 => test1A
[INFO] Running com.javabydeveloper.util.Parallel2_Test
ForkJoinPool-1-worker-7 => test2B
ForkJoinPool-1-worker-5 => test2A
ForkJoinPool-1-worker-3 => test2C
[INFO] Running com.javabydeveloper.util.Parallel3_Test
ForkJoinPool-1-worker-7 => test3A
ForkJoinPool-1-worker-5 => test3B
ForkJoinPool-1-worker-3 => test3C
[INFO]
[INFO] Results:
[INFO]
[WARNING] Tests run: 11, Failures: 0, Errors: 0, Skipped: 2
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

2. @Execution annotation

@Execution annotation is used to change the mode of test class or test methods individually. There are 2 modes, CONCURRENT mode and SAME_THREAD mode.

SAME_THREAD : Force execution in the same thread used by the parent. 

CONCURRENT : Execute concurrently unless a resource lock forces execution in the same thread.

Let’s have a look in to following dynamic tests example.

public class Junit5_Dynamic_Tests_Parallel_Test {
	
	@Execution(ExecutionMode.CONCURRENT)
	@TestFactory
	Collection<DynamicTest> test_parallel_dynamictests1() {
		
	    return Arrays.asList(
	            dynamicTest("1st dynamic test", () -> { 
	            	assertTrue(MathUtil.isPrime(13)); 
	            	System.out.println(Thread.currentThread().getName()+" => 1st dynamic test");
	            }),
	            dynamicTest("2nd dynamic test", () -> {
	            	assertEquals(5, MathUtil.devide(25, 5));
	            	System.out.println(Thread.currentThread().getName()+" => 2nd dynamic test");
	            }),
	            dynamicTest("3rd dynamic test", () -> { 
	            	assertEquals(12, MathUtil.add(7, 5)); 
	            	System.out.println(Thread.currentThread().getName()+" => 3rd dynamic test");
	            })
	        );
	  }
	
	@Execution(ExecutionMode.SAME_THREAD)
	@TestFactory
	Collection<DynamicTest> test_parallel_dynamictests2() {
		
	    return Arrays.asList(
	            dynamicTest("4th dynamic test", () -> { 
	            	assertTrue(MathUtil.isPrime(13)); 
	            	System.out.println(Thread.currentThread().getName()+" => 4th dynamic test");
	            }),
	            dynamicTest("5th dynamic test", () -> {
	            	assertEquals(5, MathUtil.devide(25, 5));
	            	System.out.println(Thread.currentThread().getName()+" => 5th dynamic test");
	            }),
	            dynamicTest("6th dynamic test", () -> { 
	            	assertEquals(12, MathUtil.add(7, 5)); 
	            	System.out.println(Thread.currentThread().getName()+" => 6th dynamic test");
	            })
	        );
	  }
}

Output :

You will notice that tests generated by test_parallel_dynamictests2() are executed by same thread and the tests generated by other method test_parallel_dynamictests1() are run by different threads concurrently.

ForkJoinPool-1-worker-5 => 1st dynamic test
ForkJoinPool-1-worker-1 => 2nd dynamic test
ForkJoinPool-1-worker-7 => 3rd dynamic test
ForkJoinPool-1-worker-3 => 4th dynamic test
ForkJoinPool-1-worker-3 => 5th dynamic test
ForkJoinPool-1-worker-3 => 6th dynamic test

3. Synchronization

If tests running in parallel with a shared resource ( for example System properties ), the results will be unpredictable. You can notice this behavior in following example.

The @ResourceLock annotation provides declarative synchronization mechanism for the test classes and test methods, and allows you to declare that a test class or method uses a specific shared resource that requires synchronized access to ensure reliable test execution.

@ResourceLock annotation takes two arguments, one is String value to specify that uniquely identifies the shared resource and another is ResourceAccessMode to specify the mode of access to the resource. A resource value can be a predefined or user defined and Access mode can be READ and READ_WRITE

Predefined Resources :

  1. Resources.SYSTEM_PROPERTIES – Represents Java’s system properties. Value is java.lang.System.properties
  2. Resources.SYSTEM_OUT – Represents the standard output stream of the current process. Value is java.lang.System.out
  3. Resources.SYSTEM_ERR – Represents the standard error stream of the current process. Value is java.lang.System.err
  4. Resources.LOCALE – The default locale for the current instance of the JVM. Value is java.util.Locale.default
  5. Resources.TIMEZONE – The default time zone for the current instance of the JVM. Value is java.util.TimeZone.default

A very nice example for predefined Resources you can find in Junit 5 document. The following example is to demonstrate user defined resource. A simple container class to hold global users.

public class GlobalUsers {
	
	private static Map<Integer, String> GLOBAL_USERS = new HashMap<>();
	
	public static String get(int id) {
		return GLOBAL_USERS.get(id);
	} 
	
	public static void add(int id, String user) {
		GLOBAL_USERS.put(id, user);
	}
	
	public static void update(int id, String user) {
		GLOBAL_USERS.put(id, user);
	}

	public static void remove(int id) {
		GLOBAL_USERS.remove(id);
	}
	
	public static void clear() {
		GLOBAL_USERS.clear();
	}
	
	public static Collection<String> getUsers() {
		return GLOBAL_USERS.values();
	}	
	
	public static void setUsers(Map<Integer, String> users) {
		GLOBAL_USERS = users;
	}
}

Test Case for GlobalUsers class :

@Execution(ExecutionMode.CONCURRENT)
public class MySharedResourceDemo_Test {

	final static String GLOBAL_USERS = "com.javabydeveloper.util.GlobalUsers.users";
	
	@BeforeEach
    void initiate() {
        GlobalUsers.clear();
    }
	
	@Test
    @ResourceLock(value = GLOBAL_USERS, mode = ResourceAccessMode.READ)
    void isEmpty_Test() {
    	System.out.println("isEmpty_Test() : "+GlobalUsers.getUsers());
        Assertions.assertTrue(GlobalUsers.getUsers().isEmpty());
    }
	
	@Test
    @ResourceLock(value = GLOBAL_USERS, mode = ResourceAccessMode.READ_WRITE)
    void add_Test() {
		GlobalUsers.add(1, "peter");
    	System.out.println("add_Test() : "+GlobalUsers.getUsers());
        Assertions.assertEquals("peter", GlobalUsers.get(1));
    }
	
	@Test
    @ResourceLock(value = GLOBAL_USERS, mode = ResourceAccessMode.READ_WRITE)
    void update_Test() {
		GlobalUsers.update(1, "john");
    	System.out.println("update_Test() : "+GlobalUsers.getUsers());
        Assertions.assertEquals("john", GlobalUsers.get(1));
    }
	
	@Test
    @ResourceLock(value = GLOBAL_USERS, mode = ResourceAccessMode.READ_WRITE)
    void remove_Test() {
		GlobalUsers.add(2, "Anand");
		GlobalUsers.remove(2);
    	System.out.println("remove_Test() : "+GlobalUsers.getUsers());
        Assertions.assertNull(GlobalUsers.get(2));
    }
}

Output :

All tests passed, following is output.

isEmpty_Test() : []
add_Test() : [peter]
update_Test() : [john]
remove_Test() : []

You just remove or comment all @ResourceLock annotation from the above test case and run the test. You will see unpredictable behavior of results, some times may pass or fail the tests. In my case I got following results.

add_Test() : [peter]
isEmpty_Test() : [peter]
update_Test() : [john]
remove_Test() : [peter]

Note :

If you are running your tests in parallel using Maven with surefire plugin, your test classes names must ends with “Test”. I haven’t seen this issue in gradle. (tested with surefire plugin version 2.22.2)

You can checkout source code from our github repository.

You also might be interested in following examples :

  1. Junit 5 Dynamic Tests and @TestFactory annotation.
  2. Junit 5 tags and filter test cases for execution.
  3. Junit 5 Timeout tests, fail tests if not completed within time.
  4. Junit 5 Repeated tests and display Repetition info.
  5. Junit 5 parameterized tests with different argument sources.
  6. Creating a Custom ParameterResolver and built in Junit 5 Parameter Resolvers.
Satish Varma
Satish Varmahttps://javabydeveloper.com
Satish is post graduated in master of computer applications and experienced software engineer with focus on Spring, JPA, REST, TDD and web development. Follow him on LinkedIn or Twitter or Facebook

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay in Touch

Categories