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

Looking for a book that defines and solves most common data engineering problems? I wrote
one on that topic! You can read it online
on the O'Reilly platform,
or get a print copy on Amazon.
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
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.
Consulting

With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects