What’s New in JUnit 5.1

Billy Korando Effective Automated Testing With Spring Series, Java, Series, Technology Snapshot Leave a Comment

This article is part of a blog series on automated testing in promotion for my new Pluralsight course Effective Automated Testing with Spring.

It is hard to believe that JUnit 5 has been out for five months! Already we have our first feature release. There are quite a few changes in 5.1 and you can see them all in the release notes.

In this article, we focus on a few of the JUnit 5.1 changes that I think are the most impactful to the day-to-day tasks of writing automated tests, including improvements to conditionally disabling and enabling tests, improvements to parameterized tests, and programmatic extensions.

Improvements to Conditionally Disabling and Enabling Tests

In my previous article on JUnit 5, I covered the enhancements to filtering and conditionally disabling tests in JUnit 5. In it, I wrote a custom ExecutionCondition to disable a test by OS. While there might be times when implementing your own custom disabling logic is necessary, with 5.1 the JUnit team added a lot of implementations that should cover a large amount of the common reasons test should be disabled. One of them being @DisabledOnOs.

@Test
@DisabledOnOs(OS.MAC)
public void testFindByNullRoomType() {
    RoomRepo repo = mock(RoomRepo.class);
    RoomServiceImpl service = new RoomServiceImpl(repo, roomTypes);
    verify(repo, times(0)).findRoomsByRoomType(any());
    RoomServiceException e = assertThrows(RoomServiceException.class, () -> service.findRoomsByType("NOT FOUND"));
    assertEquals("Room type: NOT FOUND not found!", e.getMessage());
}

Just like any other feature, this behavior can easily be extended. So, if you wanted to create an annotation that disabled tests on all *nix based systems, it’s pretty easy.

@Target({ TYPE, METHOD })
@Retention(RetentionPolicy.RUNTIME)
@DisabledOnOs({OS.LINUX, OS.MAC})
public @interface DisabledOnNixSystems {

}

Alternatively, JUnit also added functionality to support conditionally enabling tests as well. So if instead you want a test to only run on *nix based systems, that’s easy now too.

@Target({ TYPE, METHOD })
@Retention(RetentionPolicy.RUNTIME)
@EnabledOnOs({OS.LINUX, OS.MAC})
public @interface EnabledOnNixSystems {

}

Along with providing some common Enabled/Disabled implementations, the new @EnabledIf/@DisabledIf allow for passing-in strings of regexs and even JavaScript for more dynamic conditional disabling of tests. Disabling a test by day of week, say on Fridays, is quite easy and straightforward with@DisabledIf.

@Test
@DisabledIf("new Date().getDay() === 5")
public void testFindByInvalidRoomType() {
   ...
}

For more information on conditionally enabling and disabling test, check out the JUnit 5 user guide here.

Improvements to Parameterized Tests

A major focus of the 5.1 release was improving the experience of writing parameterized tests. One improvement is that when using @MethodSource, you no longer have to pass-in a method name if your method source is the same name as the test you are running.

public class JUnit5ParameterizedTest {

   @ParameterizedTest
   @MethodSource
   public void verifyDateValidation(DateValidationBean dateValidation) {
      //Test some stuff
   }

   static Stream<DateValidationBean> verifyDateValidation() {
      //Provide some data
   }

	
}

There were also many improvements made to implicit type conversion when using the @CsvSource and @ValueSource including; URI, File, and Locale, as shown below. A full list can be seen here.

public class JUnit5ParameterizedTestImprovements {

	@ParameterizedTest
	@ValueSource(strings = { "https://google.com", "https://junit.org/" })
	public void callSomeUrls(URI uri) throws IOException {
		RestTemplate restTemplate = new RestTemplate();

		assertEquals(HttpStatus.OK,
				restTemplate.getRequestFactory().createRequest(uri, HttpMethod.GET).execute().getStatusCode());
	}

	@ParameterizedTest
	@ValueSource(strings = { "src/main/resources/testFile1.txt", "src/main/resources/testFile2.txt" })
	public void lookupSomeFiles(File file) {
		assertTrue(file.exists());
	}

	@ParameterizedTest
	@ValueSource(strings = { "en", "jp" })
	public void locales(Locale locale) {
		if (locale.getLanguage().equals("en")) {
			assertEquals("English", locale.getDisplayLanguage());
		} else {
			assertEquals("jp", locale.getDisplayLanguage());
		}
	}
}

Programmatic Extensions

One tool I have been getting really interested in lately is TestContainers (see link to get more familiar). It is a tool for spinning up and tearing down a Docker container within a JUnit test.

See Also:  Encrypting Working Files Locally in Spring Batch

Let’s look at how we can configure TestContainers to restart a customer container for every test using programmatically-defined extensions. This ensures we are always working with a clean container.

Below I am using @RegisterExtension to register my custom SpringTestContainersExtension. Because I need to pass information to the Spring application context, which occurs during the postProcessTestInstance phase, my extension must be static. However, extensions can be instance fields as well.

In the constructor for the SpringTestContainersExtension, I pass-in an instance of PostgreSQLContainer, which is using a custom container image, and also the boolean value of true, stating the container should be restarted for each test.

@ContextConfiguration(classes = { HotelApplication.class }, initializers = ITCustomerJUnit5Repo.Initializer.class)
@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
public class ITCustomerJUnit5Repo {

	public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
		@Override
		public void initialize(ConfigurableApplicationContext applicationContext) {
			TestPropertyValues.of("spring.datasource.url=" + postgres.getJdbcUrl(), //
					"spring.datasource.username=" + postgres.getUsername(), //
					"spring.datasource.password=" + postgres.getPassword()) //
					.applyTo(applicationContext);
		}
	}

	private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres-hoteldb:latest").withExposedPorts(PostgreSQLContainer.POSTGRESQL_PORT);

	@RegisterExtension
	static SpringTestContainersExtension extension = new SpringTestContainersExtension.Builder().container(postgres)
			.restartContainerAfterEachTest(true).build();

	@Autowired
	private CustomerRepo repo;
	@MockBean
	private CustomerService customerService;

	private SimpleDateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd");

	@Test
	public void testRetrieveCustomerFromDatabase() {

		Customer customer = repo.findById(1L).get();

		assertEquals("John", customer.getFirstName());
		assertEquals("Doe", customer.getLastName());
		assertEquals("Middle", customer.getMiddleName());
		assertEquals("", customer.getSuffix());
		assertEquals("2017-10-30", dateFormat.format(customer.getDateOfLastStay()));
	}

	@Test
	public void testAddCustomerToDB() throws ParseException {

		Customer customer = new Customer(10L, "Princess", "Carolyn", "Cat", "", dateFormat.parse("2018-01-30"));
		
		repo.save(customer);

		assertEquals(3, repo.count());
	}

	@Test
	public void testCountNumberOfCustomersInDB() {

		assertEquals(2, repo.count());
	}
}

In my custom extension I am extending the SpringExtension. For the most part, the default behavior of SpringExtension is fine. However I do need to make some changes for stopping and starting a Docker container. To do this I will override afterAll, postProcessTestInstance, and afterEach.

In order of how a test would be executed, in postProcessTestInstance, I start up the container and ensure it is available when the spring context is being initialized. postProcessTestInstance is run before every test case, so I want to check if the container is already running before attempting to start it. If I don’t do that, I will always get a new container for every test which may not be something I want.

See Also:  OWASP Dependency Check for Vulnerability Reporting

In afterEach, I check the restartContainerForEveryTest field to see if I want a new container for every test case, and if so, stop the container I just used in the previous test case.

Finally in afterAll, if I wasn’t restarting a container after every test case, the container gets shutdown after all test cases in a class have executed.

public class SpringTestContainersExtension extends SpringExtension {

	private GenericContainer<?> container;
	private boolean restartContainerForEveryTest = false;

	public SpringTestContainersExtension(GenericContainer<?> container) {
		this.container = container;
	}

	public SpringTestContainersExtension(GenericContainer<?> container, boolean restartContainerForEveryTest) {
		this.container = container;
		this.restartContainerForEveryTest = restartContainerForEveryTest;
	}

	@Override
	public void afterAll(ExtensionContext context) throws Exception {
		if (container.isRunning()) {
			container.stop();
		}
		super.afterAll(context);
	}

	@Override
	public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
		if (!container.isRunning()) {
			container.start();
		}
		super.postProcessTestInstance(testInstance, context);
	}

	@Override
	public void afterEach(ExtensionContext context) throws Exception {
		if (restartContainerForEveryTest) {
			container.stop();
		}
		super.afterEach(context);
	}

}

Conclusion

It is great to see so much active support and work being done on JUnit 5. In less than six months after the initial release, we already have our first feature release of JUnit 5 which adds a number of new features and improves upon existing ones. If you haven’t started using JUnit 5 yet, I would highly recommend getting started as JUnit 5 offer a lot of improvements to writing automated tests in Java.

All the code examples used in the article can be found here.

Automated Testing Series

  1. Without Automated Testing You Are Building Legacy
  2. Four Common Mistakes That Make Automated Testing More Difficult
  3. Encouraging Good Behavior with JUnit 5 Test Interfaces
  4. Conditionally Disabling and Filtering Tests in JUnit 5
  5. This Post -> What’s New in JUnit 5.1
  6. Fluent Assertions with AssertJ
  7. Why Am I Writing This Test?
  8. What’s New in JUnit 5.2

What Do You Think?