A complete guide to JUnit 5 with Java and Gradle

In this JUnit tutorial series, we will discuss the features of JUnit 5 along with detailed JUnit examples with Java and Gradle.
While writing these tutorials we have used Java 13 and IntelliJ IDEA. Although the minimum java version required for JUnit 5 is Java 8, it can be used safely with any higher version.

JUnit is an open source project which is hosted at Github.

1. Why JUnit 5?

Junit5 Tutorial

JUnit 5 is one of the most widely used frameworks for testing java applications. With the introduction of Streams and Lambda functions in JDK 8, JUnit 5 also aims to adapt to the new powerful features to provide support to Java 8 features. This is the reason why Java 8 is required to create and execute tests in JUnit 5.

It is not only about the support to new language features but JUnit 5 also introduces a lot of other important features such as Parameterized Tests, Nested Tests, Dynamic Tests, Display Name(a method to provide detail and parameter-based naming to test method)


2. JUnit 5 Architecture

Unlike JUnit4, JUnit 5 is composed of several different modules from three different sub-projects:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

2.1. JUnit Platform

Various IDEs, build tools or plugins need to include and extend platform APIs in order to be able to launch JUnit tests. It defines the TestEngine API for developing new testing frameworks that run on the platform.
It also provides a Console Launcher to launch the platform from the command line and build plugins for Gradle and Maven.

2.2. JUnit Jupiter

New annotations, extension models and new programming paradigms for writing the tests are defined in this module. It also contains  TestEngine  implementation to run tests written with these annotations.

2.3. JUnit Vintage

Its primary purpose is to support running JUnit 3 and JUnit 4 written tests on the JUnit 5 platform. It’s there for backward compatibility.


3. Annotations

Following annotations are present to be used in a test. Reference JUnit 5 official documentation.

In the table below I personally use top 11 (i.e. upto @ExtendWith) annotations on almost a daily basis.

AnnotationsDescription
@TestThe annotated methods run as a unit test. In JUnit Jupiter, tests are operated based on their own dedicated annotations and hence it does not have any parameters. It is inherited unless overridden.
@ParameterizedTestThe annotated method expects some parameters and is marked as a test method. It is inherited unless overridden.
@DisplayNameUse to define a custom name to the test method. This annotation is not inherited.
@BeforeEachUse to run a common code before( eg setUp) each test method execution. analogous to JUnit 4’s @Before. It is inherited unless overridden.
@AfterEachUse to run a common code after( eg tearDown) each test method execution. analogous to JUnit 4’s @After. It is inherited unless overridden.
@BeforeAllUse to run once per class before any test execution. analogous to JUnit 4’s @BeforeClass. Such methods are inherited and must be static.
@AfterAllUse to run once per class after all test are executed. analogous to JUnit 4’s @AfterClass. Such methods are inherited and must be static.
@NestedUse to mark a non static nested class so that all tests written inside are executed. @BeforeAll and @AfterAll methods cannot be used directly in a @Nested test class unless the “per-class” test instance lifecycle is used. This annotations are not inherited.
@TagTags used for filtering tests on class or method level. This annotation is inherited at the class level but not at the method level.
@DisabledDisables a test class or test method; analogous to JUnit 4’s @Ignore. This annotations are not inherited.
@ExtendWithUsed to register extensions declaratively. This annotation is inherited.
@RepeatedTestThe annotated method is a test template for a repeated test. It is inherited unless overridden.
@TestFactoryThe annotated method is a test factory for dynamic tests. It is inherited unless overridden.
@TestTemplateIndicates that a method is a template for test cases designed to be invoked multiple times depending on the number of invocation contexts returned by the registered providers. It is inherited unless overridden.
@TestMethodOrderDefines the order of test method execution. This annotation is inherited.
@TestInstanceUsed to configure the test instance lifecycle for the annotated test class. This annotation is inherited.
@DisplayNameGenerationUse to define a custom display name generator for the annotated test class. This annotation is inherited.
@TimeoutUsed to fail a test it it takes more than defined time .This annotation is inherited.
@RegisterExtensionUsed to register extensions programmatically via fields. Such fields are inherited unless they are shadowed.
@TempDirSupplies temp directory with field injection.
Junit5 Annotations

4. Create Java-Junit Project with Gradle

Creating a Java application with Gradle using CLI is pretty straight forward as shown below. Reference documentation

Here is the list of commands and options that we have used to create a Gradle project with JUnit5. We are using Kotlin DSL for Gradle.

gradle init

Welcome to Gradle 6.7.1!

Here are the highlights of this release:
 - File system watching is ready for production use
 - Declare the version of Java your build requires
 - Java 15 support

For more details see https://docs.gradle.org/6.7.1/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 2

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit 4) [1..4] 4

Project name (default: JUnit5): Junit5-gradle-app     
Source package (default: Junit5.gradle.app): com.codingeek

> Task :init
Get more help with your project: https://docs.gradle.org/6.7.1/samples/sample_building_java_applications.html

BUILD SUCCESSFUL in 1m 32s
2 actionable tasks: 2 executed

The dependencies added for the JUnit Jupiter are as per the output below. We have to add an additional dependency for junit-jupiter-params to add some features like parameterized tests.

    // Additional dependency to be added.
    testImplementation("org.junit.jupiter:junit-jupiter-params:5.7.0")
    // Use JUnit Jupiter API for testing.
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
    // Use JUnit Jupiter Engine for testing.
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
    // This dependency is used by the application.
    implementation("com.google.guava:guava:29.0-jre")

5. Basic Test Class Structure

So let us see how basic annotations and the basic structure of a test class looks like. In this section, we will try to use the basic and the most common annotations that we require for writing a Junit test.

In the example below notice the output of different annotations like @Test, @BeforeAll, @AfterAll, @Disabled, @ParameterizedTest etc.

package com.codingeek;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class SampleTest {
  private final int expected = 10;
  private final int actual = 5 * 2;

  @BeforeAll
  static void runOnceBeforeAllTests() {
    System.out.println("@BeforeAll executed");
  }

  @BeforeEach
  void runBeforeEveryTest() {
    System.out.println("@BeforeEach executed");
  }

  @Test
  void testMethod() {
    System.out.println("[email protected] executed====");
    Assertions.assertEquals(expected, actual);
  }

  @Disabled
  @Test
  void testDisabledMethod() {
    System.out.println("=====Disabled @Test executed====");
    Assertions.assertEquals(expected, actual);
  }

  @ParameterizedTest
  @ValueSource(ints = {1, 2, 3})
  void testParameterizedMethod(int number) {
    System.out.println("[email protected] executed==== value "+number);
    Assertions.assertTrue(number > 0);
  }

  @AfterEach
  void runAfterEveryTest() {
    System.out.println("@AfterEach executed");
  }

  @AfterAll
  static void runOnceAfterAllTests() {
    System.out.println("@AfterAll executed");
  }
}
Output:-
@BeforeAll executed
@BeforeEach executed
[email protected] executed==== value 1
@AfterEach executed
@BeforeEach executed
[email protected] executed==== value 2
@AfterEach executed
@BeforeEach executed
[email protected] executed==== value 3
@AfterEach executed
@BeforeEach executed
[email protected] executed====
@AfterEach executed
@AfterAll executed

Read More: JUnit Test Lifecycle


6. Assertions

To test the data and behavior in the unit tests we use assertions to validate the expected and the actual output of a test case and based on these assertions it is decided whether a test is a success or a failure. In JUnit5 for the sake of simplicity, all JUnit Jupiter assertions are static methods in the org.junit.jupiter.Assertions class e.g. assertEquals()assertNotEquals().

Some usage of JUnit5 assertions are as follows

// Import statement
import static org.junit.jupiter.api.Assertions.*;
import static java.time.Duration.ofMillis;

// test multiple asserts or groouping them together
assertAll("website",
            () -> assertEquals("Codingeek", website.name()),
            () -> assertEquals(".com", website.domain())
        );
assertTrue("Coddingeek.com".contains(".com"));

// The following assertion fails because it expects it to run in 10 ms
assertTimeout(ofMillis(10), () -> {
    // Simulate task that takes more than 10 ms.
    Thread.sleep(100);
});

Read More: JUnit Assertions


7. Assumptions

Sometimes we want to execute the tests only if certain conditions are fulfilled and JUnit Jupiter has a set of assumption methods that helps us with the same. These methods are developed to support Java 8 lambda expressions and method references.

If an assumption is not valid then the test is aborted or skipped and will be shown as skipped in the test report.

JUnit Jupiter Assumptions class has three such methods:  assumeFalse()assumeTrue() and assumingThat() in org.junit.jupiter.api.Assumptions class.

Explanation- In the example below, the output is generated only when the assumeTrue statement is valid.

  @ParameterizedTest
  @ValueSource(ints = {1, 2, 3})
  void testParameterizedMethodAssume(int number){

// This statement will skip the test when number == 2
    Assumptions.assumeTrue(number != 2);

    System.out.println("[email protected] executed==== value "+number);
    assertTrue(number > 0);
  }
Output:-
[email protected] executed==== value 1
[email protected] executed==== value 3

Read More: JUnit Assumptions – assumeFalse(), assumeTrue() and assumingThat()


8. Parameterized Test

Generally, we want to test a particular feature over multiple values like boundary values, in range values, invalid values but the code to test all these scenarios is similar. This type of testing also makes our tests more robust and gives more confidence in our code.

To achieve this requirement of testing a single piece of test over multiple values we can use the Parameterized tests. In short, these tests make it possible to run a test multiple times with different arguments. We use @ParameterizedTest annotation.

We have to provide at least one source against which the test will run. Possible annotations to support Parameterized tests are –

  1. @ValueSource
  2. Null and Empty Sources
  3. @EnumSource
  4. @MethodSource
  5. @CsvSource
  6. @CsvFileSource
  7. @ArgumentsSource

One of the examples that we have already discussed above is using @ValueSource(ints = {1, 2, 3}) like

@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testParameterizedMethodAssume(int number){
    ...... // test implementation
}

9. Dynamic Test

Using @Test annotation we define the static tests which are specified completely at the compile time. DynamicTest is a test generated during runtime. A factory method with the annotation @TestFactory annotation generates the tests.

A @TestFactory method must return a single DynamicNode or a Stream, Collection, Iterable, or Iterator of DynamicNode(or its subclasses DynamicContainer or DynamicTest) instances. JUnitException is thrown for any other return type. Apart from this, a @TestFactory method cannot be static or private.

One important difference to be noted here is that even though with Dynamic tests multiple tests are run and they are also present separately in reports. @BeforeEach and @AfterEach method runs only once for one @TestFactory and that is one major difference between @ParameterizedTests and Dynamic Tests.

  @TestFactory
  Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
    // Stream of palindromes to check
    Stream<Integer> inputStream = Stream.of(1, 2, 3);

    // Generates display names like: Test for number 1
    Function<Integer, String> displayNameGenerator = number -> "Test for number " + number;

    // Executes tests based on the current input value.
    ThrowingConsumer<Integer> testExecutor = number -> assertTrue(number > 0);

    // Returns a stream of dynamic tests.
    return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
  }

10. Conclusion

Junit5 and Jupiter Params have a lot of new, improved, and interesting features which in my opinion improves the quality of tests and also help in covering multiple scenarios with ease.

In today’s world of CI CD deployments, tests plays a very important role. All the features like ParameterizedTests, Nested Test, Dynamic Tests, improved ways of assertion etc helps in better structuring of the tests and at the same time keeping them clean and robust.

Complete code samples are present on Github project.


An investment in knowledge always pays the best interest. I hope you like the tutorial. Do come back for more because learning paves way for a better understanding

Do not forget to share and Subscribe.

Happy coding!! 😊

Recommended -

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x