Introduction to JUnit 5

Versions: JUnit 5.0.0-M2

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:

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.

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!