Introduction to JUnit 5

on waitingforcode.com

Introduction to JUnit 5

JUnit is an incontestable part of each Java project. Step by step this framework comes closer to its new major version - 5.

This post is based on the 2nd milestone (5.0.0-M2) of JUnit 5. The final version isn't officially released yet. This post is only a small exploratory research on eventual new features of this framework.

The first part of this post describes some new features introduced in JUnit 5. The second one shows them through...test cases.

Junit 4 vs Junit 5

Even if Junit 5 brings a possibility to execute tests defined in the previous version of the framework, it brings also a lot of changes:

  • Extensions

    JUnit 5 proposes new method to deal with tests lifecycle - extensions. Each interceptable event, such as: test(s) execution (before/after), exception handling, method parameters resolving, test classes post-processing (dependencies injection etc.).

  • Lifecycle annotations changed

    Lifecycle annotations (@BeforeClass, @AfterClass, @Before, @After) were renamed to be more meaningful. Thus, the annotations for the method executed separetely before and after each tested method are, respectively, @BeforeEach and @AfterEach. The annotations executed before and after the execution of all tested methods (ie. before the execution of the first test and after the execution of the last) are since called @BeforeAll and @AfterAll.

  • Possibility to define dynamic tests

    Dynamic tests are the tests constructed at runtime. With this feature, based on Java's functional interfaces, we can build test suites similarly to the parametrized tests with JUnit-dataprovider in Junit 4. To do that, we need to use new annotation, @TestFactory instead of @Test, and make tested method return Stream, Collection, Iterable or Iterator instances.

  • @Category replaced by @Tag

    In Junit 4, @Category annotation was used to mark a test as belonging to one or more group of tests. Thanks to that, they can be executed selectively (for example only tests from one particular group). In Junit 5 @Category and @Categories annotations were replaced by, respectively, @Tag and @Tags, keeping the same purpose.

  • @Disable instead of @Ignore to ignore tests

    To ignore tests in Junit 5 a new annotation must be used - @Disable.

  • Readability improvement

    Test methods defined in camelCase or snake_case to improve the readability ? No, JUnit 5 goes further and proposes the annotation @DisplayName which can give even more readable context about executed test. As its name indicates, this annotation defines test name displayed in IDE and reporting tools.

  • Methods can have parameters

    Junit 5 brings new feature to handle tests with parameters, represented by ParameterResolver. Two built-in resolvers, TestReporter and TestInfo, can be used to export test data (as used parameters).

Of course, there are plenty other changes, such as nested classes tests or new assertion methods (assertAll). But the listed ones seem to be more often used than the others.

Test examples in Junit 5

Below tests show the use of: new annotations (@Disabled, @DisplayName), extensions (CarParameterResolver, TestTimer) and dynamic tests (@TestFactory):

// 2 extensions to show lifecycle management
public class TestTimer implements BeforeAllCallback, AfterAllCallback {
  @Override
  public void afterAll(ContainerExtensionContext context) throws Exception {
    System.out.println("[TestTimer] Tests ended at: " + LocalDateTime.now());
  }

  @Override
  public void beforeAll(ContainerExtensionContext context) throws Exception {
    System.out.println("[TestTimer] Tests started at: " + LocalDateTime.now());
  }
}

public class CarParameterResolver implements ParameterResolver {

  @Override
  public boolean supports(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    return parameterContext.getParameter().getType()
      .getName().equals("com.waitingforcode.Car");
  }

  @Override
  public Car resolve(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    if (parameterContext.getParameter().getAnnotation(ParamQualifier.class).value().equals("notOld")) {
      return new Car("Test car", LocalDate.now().getYear()-5);
    }
    // Otherwise consider that tested car is the old one
    return new Car("Test car", 1900);
  }
}

// Helper annotation to qualify object to create in parameter resolvers
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamQualifier {

  String value();

}

// Test cases showing new features of JUnit 5
@ExtendWith(TestTimer.class)
public class ParameterResolverTest {

  @Test
  @DisplayName("Car produced 5 years ago shouldn't be considered as old")
  @ExtendWith(CarParameterResolver.class)
  public void should_detect_that_car_is_not_old(@ParamQualifier("notOld") Car notOldCar) {
    assertFalse(CarDetectors.isOld(notOldCar), "Car should not be detected as old");
  }

  @Test
  @DisplayName("Car produced in 1900 should be considered as old")
  @ExtendWith(CarParameterResolver.class)
  public void should_detect_that_car_is_old(@ParamQualifier("old") Car oldCar) {
    // Test fails expressively to show @DisplayName feature
    assertFalse(CarDetectors.isOld(oldCar), "Car should be detected as old");
  }

  @Test
  @Disabled
  public void should_not_execute_failing_and_disabled_test() {
    Car car = null;
    assertNotNull(car, "Car should not be null");
  }

  @Test
  public void should_sleep_to_show_test_timer_extension() throws InterruptedException {
    Thread.sleep(2_000L);
  } 
}

public class TestFactoryTest {

  @TestFactory
  @DisplayName("Dynamic test example")
  Collection<DynamicTest> should_detect_cars_as_old() {
    return Arrays.asList(
      dynamicTest(">> Car from 1800 <<",
        () -> checkIfCarIsOld(new Car("1", 1800))),
      dynamicTest(">> Car from 1900< <",
        () -> checkIfCarIsOld(new Car("1", 1900))),
      dynamicTest(">> Car from current year <<",
        () -> checkIfCarIsOld(new Car("1", LocalDate.now().getYear())))
    );
  }

  private void checkIfCarIsOld(Car car) {
    assertAll(
      () -> assertNotNull(car, "Construction year should be set"),
      () -> assertTrue(CarDetectors.isOld(car), "Car should be detected as old but was not")
    );
  }

}



public final class CarDetectors {

  private CarDetectors() {
    // prevents init
  }

  public static boolean isOld(Car car) {
    return LocalDate.now().getYear() - car.getConstructionYear() >= 20;
  }

}

public class Car {

  private final String name;

  private final int constructionYear;

  public Car(String name, int constructionYear) {
    this.name = name;
    this.constructionYear = constructionYear;
  }

  public String getName() {
    return name;
  }

  public int getConstructionYear() {
    return constructionYear;
  }
}

After executing them with gradle test, you can observe improved readability thanks to @DisplaName as well as new execution (TestTimer) feature [the failure is voluntary]:

:junitPlatformTest
nov. 12, 2016 1:00:17 PM org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
INFOS: Discovered TestEngines with IDs: [junit-jupiter]
[TestTimer] Tests started at: 2016-11-12T13:00:17.825
[TestTimer] Tests ended at: 2016-11-12T13:00:19.847

Failures (2):
  JUnit Jupiter:ParameterResolverTest:Car produced in 1900 should be considered as old
    JavaMethodSource [javaClass = 'com.waitingforcode.ParameterResolverTest', javaMethodName = 'should_detect_that_car_is_old', javaMethodParameterTypes = 'com.waitingforcode.Car']
    => org.opentest4j.AssertionFailedError: Car should be detected as old
  JUnit Jupiter:TestFactoryTest:Dynamic test example:>> Car from current year <<
    JavaMethodSource [javaClass = 'com.waitingforcode.TestFactoryTest', javaMethodName = 'should_detect_cars_as_old', javaMethodParameterTypes = '']
    => org.opentest4j.MultipleFailuresError: Multiple Failures (1 failure)
	Car should be detected as old but was not

Test run finished after 2081 ms
[         7 tests found      ]
[         1 tests skipped    ]
[         6 tests started    ]
[         0 tests aborted    ]
[         4 tests successful ]
[         2 tests failed     ]
[         0 containers failed]

:junitPlatformTest FAILED

The post shows new features introduced in Junit 5 Milestone 2. Extensions and dynamic tests seem to bring much more flexibility when defining test cases. According to defined milestones, next releases should bring even more features: parametrized tests, scenario tests or repeated tests.