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?
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.
Annotations | Description |
---|---|
@Test | The 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. |
@ParameterizedTest | The annotated method expects some parameters and is marked as a test method. It is inherited unless overridden. |
@DisplayName | Use to define a custom name to the test method. This annotation is not inherited. |
@BeforeEach | Use to run a common code before( eg setUp) each test method execution. analogous to JUnit 4’s @Before. It is inherited unless overridden. |
@AfterEach | Use to run a common code after( eg tearDown) each test method execution. analogous to JUnit 4’s @After. It is inherited unless overridden. |
@BeforeAll | Use to run once per class before any test execution. analogous to JUnit 4’s @BeforeClass. Such methods are inherited and must be static. |
@AfterAll | Use to run once per class after all test are executed. analogous to JUnit 4’s @AfterClass. Such methods are inherited and must be static. |
@Nested | Use 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. |
@Tag | Tags used for filtering tests on class or method level. This annotation is inherited at the class level but not at the method level. |
@Disabled | Disables a test class or test method; analogous to JUnit 4’s @Ignore. This annotations are not inherited. |
@ExtendWith | Used to register extensions declaratively. This annotation is inherited. |
@RepeatedTest | The annotated method is a test template for a repeated test. It is inherited unless overridden. |
@TestFactory | The annotated method is a test factory for dynamic tests. It is inherited unless overridden. |
@TestTemplate | Indicates 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. |
@TestMethodOrder | Defines the order of test method execution. This annotation is inherited. |
@TestInstance | Used to configure the test instance lifecycle for the annotated test class. This annotation is inherited. |
@DisplayNameGeneration | Use to define a custom display name generator for the annotated test class. This annotation is inherited. |
@Timeout | Used to fail a test it it takes more than defined time .This annotation is inherited. |
@RegisterExtension | Used to register extensions programmatically via fields. Such fields are inherited unless they are shadowed. |
@TempDir | Supplies temp directory with field injection. |
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("=====@Test 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("=====@ParameterizedTest 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 =====@ParameterizedTest executed==== value 1 @AfterEach executed @BeforeEach executed =====@ParameterizedTest executed==== value 2 @AfterEach executed @BeforeEach executed =====@ParameterizedTest executed==== value 3 @AfterEach executed @BeforeEach executed =====@Test 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("=====@ParameterizedTest executed==== value "+number); assertTrue(number > 0); }
Output:- =====@ParameterizedTest executed==== value 1 =====@ParameterizedTest 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 –
- @ValueSource
- Null and Empty Sources
- @EnumSource
- @MethodSource
- @CsvSource
- @CsvFileSource
- @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. A 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!! ?
Great post!!!
This complete guide to JUnit 5 with Java and Gradle is an invaluable resource for developers. JUnit 5 is a powerful testing framework, and this guide provides a comprehensive overview of its features and functionalities. The step-by-step instructions, code examples, and explanations make it easy to understand and implement JUnit 5 in Java projects using Gradle. Whether you’re a beginner or an experienced developer, this guide equips you with the knowledge to write effective and efficient tests. Thank you for sharing this detailed and well-structured guide, it’s a must-read for anyone looking to level up their testing skills with JUnit 5.