What’s So Great About JUnit 5?

JUnit is a unit testing framework for Java and is one of the most popular frameworks used by organizations. After we’ve been working with the 4.12 JUnit version for a while, a new and exciting version is out – JUnit 5. In this post, we will learn what JUnit framework is and which cool novelties the new version brings along.

So, what is JUnit? And why should we all want to work with it?

With JUnit we have the ability to run the tests (the code) automatically (It’s basically the automation of automation!). With JUnit we have these capabilities:

  • Annotations
  • Assertions
  • Suite Executions
  • And more!

JUnit 5 Configuration

In order to configure JUnit, you need to use a Build Management Tool such as Maven. Here are the dependencies you need to add to your project to work with JUnit 5:

JUnit5

  • junit-jupiter-api – Exposes the JUnit API.
  • junit-jupiter-engine – A dependency that defines the JUnit execution engine. 
  • junit-vintage-engine – In case we’d like to continue working with previous JUnit versions (JUnit 3 or 4), we’ll need to work with this dependency. 
  • junit-platform-launcher – Exposes the API to the configuration that the IDE usually uses. 
  • junit-platform-runner – Enables to run tests and packages written in JUnit 4. 

 

Annotations

Annotations start with the @ character and then the annotation’s name. This way we can write our classes in a testable way and define executable functions that can run by JUnit execution engine (that is built on top of Java). In JUnit 5 we have some new annotations. Here’s a summary of all the JUnit 5 supported annotations:

junit5_annotations

 

Those of you who are familiar with JUnit and are used to working with JUnit 4, will probably notice that there are some new annotations (such as: @nested) and that the old annotations now have different names (such as: @BeforeEach). Below is a code example that demonstrates working with these JUnit 5 annotations:

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JUnit5Sample
{
  @BeforeAll
  static void beforeAll()
{
    System.out.println("Executed once before all test methods in this class");
}

  @BeforeEach
  void beforeEach()
{
    System.out.println("Executed before each test method in this class");
}

  @Test
  void testMethod1()
{
    System.out.println("Test method1 executed");
}

  @Test
  void testMethod2()
{
    System.out.println("Test method2 executed");
}

  @AfterEach
  void afterEach()
{
    System.out.println("Executed after each test method in this class");
}

  @AfterAll
  static void afterAll()
{
    System.out.println("Executed once after all test methods in this class");
}
}

Now we can play with other annotations, such as DisplayName:

@Test
@DisplayName("Hello World")
 void test01()
 {
     System.out.println("Test Hello World is Invoked");
 }

The Disabled annotation will not allow executing the test it’s associated to (needed in case we temporarily do not want to execute the test):

@Test
@Disabled("Do not run this test")
 void test01()
 {
     System.out.println("Hello World");
 }

We can also implement the Disabled annotation on a class and then all the tests under that class will be disabled:

@Disabled
public class AppTest
{
    @Test
     void test01()
     {
         System.out.println("Hello World");
     }
}

The Tag annotation filters certain tests and associates tests to groups, as can be seen in the example below:

@Test
@Tag("Sanity")
 void test01()
 {
     System.out.println("Hello World");
 }

@Test
@Tag("Regression")
 void test02()
 {
     System.out.println("Hello World 2");
 }

@Test
@Tag("Sanity")
@Tag("Regression")
 void test03()
 {
     System.out.println("Hello World 3");
 }

In this example above, test01 is associated with a group of tests called Sanity, and test02 is associated with a group called Regression, whereas test03 is associated with both groups: Sanity and Regression. 

 

The RepeatedTest annotation enables executing the same test case multiple times. In addition, you can send this test case parameters in order for it to pass different values within the same test. For example: 

@Test
@RepeatedTest(5)
void test01()
{
System.out.println("Hello World");
}

Let’s send the test case parameters:

@Test
@DisplayName("My Test Name")
@RepeatedTest(value = 5, name = "{displayName} - repetition {currentRepetition} of {totalRepetitions}")
 void addNumber(TestInfo testInfo)
 {
        System.out.println("Hello World");
 }

 

Nested Classes

This is one of the new interesting features JUnit 5 has to offer – The ability to nest one class inside the other in order to create modular tests that will allow creating groups of sub-tests within tests. For instance, when you want to create tests for a sub-menu:

@DisplayName("FatherClass")
class JUnit5Sample
{
    @BeforeAll
    static void beforeAll()
   {
        System.out.println("Before all test methods");
    }
 
    @BeforeEach
    void beforeEach() 
    {
        System.out.println("Before each test method");
    }
 
    @AfterEach
    void afterEach() 
    {
        System.out.println("After each test method");
    }
 
    @AfterAll
    static void afterAll()
    {
        System.out.println("After all test methods");
    }
 
    @Nested
    @DisplayName("Child Nested Class")
    class JUnit5NestedSample
    {
        @BeforeEach
        void beforeEach()
        {
            System.out.println("Before each test method of the JUnit5NestedSample class");
        }
 
        @AfterEach
        void afterEach()
        {
            System.out.println("After each test method of the JUnit5NestedSample class");
        }
 
        @Test
        @DisplayName("Example test for method JUnit5NestedSample")
        void sampleTestForMethod() 
        {
            System.out.println("Example test for method JUnit5NestedSample");
        }
    }
}

 

Passing Parameters to Tests

You can also pass values into tests, so that this test will be repeated according to the number of parameters you’ve passed: 

class MyClass
    {
    @ParameterizedTest
    @ValueSource(strings = {"Hello", "World"})
    void test01(String message)
    {
        System.out.println(message);
    }
}

In this case, the test will be executed twice. The first time it will print the word ‘Hello’ and the second time it will print the word ‘World’.

Of course, we can also pass the test a structure of parameters like a CSV structure, as seen in the example below:

@CsvSource({"1, 1, 2", "2, 3, 5"})
void sum(int a, int b, int sum)
{
    assertEquals(sum, a + b);
}

Or even pass it an external CSV file:

@CsvSource(resources = "C:/Data/values.csv")
void sum(int a, int b, int sum)
{
    assertEquals(sum, a + b);
}

 

Assertions

Assertions are functions that allow us to check data during our tests. JUnit 5 supports all types of assertions that exist in version 4. In fact, in JUnit5 we can work with 3 types of assertions:

1. The Assertions of JUnit5 API, for example:

assertEquals(EXPECTED, ACTUAL);
assertNotEquals(EXPECTED, ACTUAL);
assertNull(null);
assertNotNull(new Object());
assertTrue(true);
assertFalse(false);

2. The assertions of Hamcrest, for example:

assertThat(true, is(true));
assertThat(false, is(false));
assertThat(ACTUAL, is(EXPECTED));
assertThat(ACTUAL, not(EXPECTED));

3. The assertions of AssertJ, for example:

assertThat(true).isTrue();
assertThat(false).isFalse();
assertThat(ACTUAL).isEqualByComparingTo(EXPECTED);
assertThat(ACTUAL).isNotEqualByComparingTo(EXPECTED);

 

Executing a Test Suite

With JUnit 5 we can also execute test suites by defining them in a separate class, for instance by choosing different packages:

@RunWith(JUnitPlatform.class)
@SelectPackages({"packageAAA","packageBBB"})
public class JUnit5SuiteExample
{
}

or even choosing specific classes for execution:

@RunWith(JUnitPlatform.class)
@SelectClasses( { MyTestsA.class, MyTestsB.class, MyTestsC.class } )
public class JUnit5SuiteExample
{
}

You can filter the execution according to packages:

@RunWith(JUnitPlatform.class)
@SelectPackages("MyPackages")
@IncludePackages("MyPackages.packageA")  // Include Package
public class JUnit5SuiteExample
{
}
@RunWith(JUnitPlatform.class)
@SelectPackages("MyPackages")
@ExcludePackages("MyPackages.packageA") // Exclude Package
public class JUnit5SuiteExample
{
}

You can also filter the execution according to classes:

@RunWith(JUnitPlatform.class)
@SelectPackages("MyClasses")
@IncludeClassName("MyClasses.classA")  // Include Class
public class JUnit5SuiteExample
{
}
@RunWith(JUnitPlatform.class)
@SelectPackages("MyClasses")
@ExcludeClassName("MyClasses.ClassA") // Exclude Class
public class JUnit5SuiteExample
{
}

And also filter according to tags (by using the Tag annotation we talked about above):

@RunWith(JUnitPlatform.class)
@SelectPackages("MyPackages")
@IncludeTags("Sanity")                 // Include Tags
public class JUnit5SuiteExample
{
}
@RunWith(JUnitPlatform.class)
@SelectPackages("MyPackages")
@ExcludeTags("Sanity")                 // Exclude Tags
public class JUnit5SuiteExample
{
}

 

Are you already working with JUnit 5? Share your experience in the comments below  😎 


Reference