Write unit tests with JUnit5 platform

March 29, 2020

by — Posted in Core Java

Hello! This article provides a solid introduction to writting Java unit tests with the JUnit5 platform.

What is unit testing?

Unit testing is a level of software testing, when we test individual software components in isolation. For example, we have UserService, that may have various connected dependencies: UserRepository component to connect with a datasource, EmailClient to communicate with remote API that will send confirmation emails. For unit testing, we isolate UserService and then mock external dependencies.

The unit testing offers us a number of benefits, to name few:

  • It increases our confidence, when we change code. If unit tests are good written and if they are run every time any code is changed, we can notice any failures, when we introduce new features
  • It serves as documentation. Certainly, documenting your code includes several tools, and unit testing is one of them – it describes an expected behaviour of your code to other developers.
  • It makes your code more reusable, as for good unit tests, code components should be modular.

These advantages are just few of numerous, that are provided us by unit testing. Now, when we defined what is unit testing and why do we use it, we are ready to move to JUnit5.

An anatomy of unit test

In this section, let overview a general structure of typical JUnit 5 unit test. Let write a small class, that will return a position (index) of number in list of integers. We will use Java Optional here to wrap return result. Observe the sample implementation:

class SearchUtil {

    private List<Integer> elements;

    SearchUtil(List<Integer> elements){
        this.elements = elements;
    }

    Optional<Integer> getPosition (Integer element){
        if (elements.contains(element)){
            return Optional.of(elements.indexOf(element));
        } else {
            return Optional.empty();
        }
    }
}

Next, let prepare a unit test. Technically, unit tests include following parts:

  1. Before methods (optional) = used to setup test environment, executed before test cases
  2. Test cases = annotated with @Test
  3. After methods (optional) = used usually to clean up environment, executed after test cases.

Take a look on the code snippet, that presents a unit test:

class SearchUtilTest {

    private static SearchUtil util;

    @BeforeAll
    static void setup() {
        List<Integer> elements = Lists.newArrayList(3,10,17,24,31,38,45,52,59,66,73,80);
        util = new SearchUtil(elements);
    }

    @Test
    void getPositionTest(){
        Optional<Integer> result = util.getPosition(31);
        Assertions.assertTrue(result.isPresent());
        Assertions.assertEquals(4, result.get());
    }

    @Test
    void noElementTest(){
        Optional<Integer> result = util.getPosition(94);
        Assertions.assertTrue(result.isEmpty());
        Assertions.assertThrows(NoSuchElementException.class, () -> result.get());
    }

    @AfterEach
    void after(){
        System.out.println("Test completed");
    }
}

Here we have following functionality:

  1. @BeforeAll-annotated method, that configures an instance of the class under test
  2. Test case to check, that utility correctly returns an index of number
  3. Test case to check situation, where element is not in the list
  4. @AfterEach-annotated method, which is executed after each test case to print a message

Before vs. after methods

You could note, that is method annotated with @BeforeAll. This is called before method, and JUnit 5 offers two before methods:

  • @BeforeAll – the static method that will be executed once before all @Test method in the current class.
  • @BeforeEach – the method that will be executed before each @Test method in the current class.

These methods are handy to setup unit test environment (for example, to configure instances). Another group of annotations, are used to create after methods. There are also two after methods:

  • @AfterAll – the static method will be executed once after all @Test methods in the current class.
  • @AfterEach – the method that will be executed after each @Test method in the current class.

Using JUnit5 Assertions API

Assertions API is a collection of utility methods that support asserting conditions in tests. There is a lot of available assertion methods, although in this post, we would focus on most important of them.

Assert that object is not null

When we need to assert, that actual object is not null, we can use the assertNotNull method. Take a look on the following code example:

@Test
void notNullTest(){
    String message = "Hello world!";
    Assertions.assertNotNull(message);
}

If object is not null, the method passes, if not – fails.

Assert equals

This group includes many methods, that all follow a general structure:

assertEquals(expected_value, actual_value, optional_message);

These methods have two required arguments and one optional argument:

  • expected_value = the result, we want to receive; required
  • actual_value = the tested value; required
  • optional_ message = String message, that would be displayed to the standard output if method is failed; optional

Values can be of primitive types: int, double, float, long, short, boolean, char, byte, as well Strings and Objects. To this group, we can add these test methods:

  • assertArrayEquals– check that expected and actual arrays are equal. Arrays are of primitive types
  • AssertFalse and AssertTrue – check that supplied boolean condition is false or true respectively
  • assertIterableEquals – same as assertArrayEquals, but for Iterables (e.g. List, Set etc)

This code snippet demonstrates how to use this type of assertions:

@Test
void equalsTest(){
  int number = 25;
  Assertions.assertEquals(25, number);
  Assertions.assertNotEquals(10, number, "Number is 10, but should not be");

  List<Integer> elements = Lists.newArrayList(3,10,17,24,31,38,45,52,59,66,73,80);
  List<Integer> values = Lists.newArrayList(3,10,17,24,31,38,45,52,59,66,73,80);
  Assertions.assertIterableEquals(elements, values);
}

Assert that exception is thrown

This is an innovation of JUnit5. In case, when you need to validate, that method under test throws a specified exception, use assertThrows method. It has two components:

  1. Expected exception to be thrown
  2. Lambda expression of Executable, that contains a code snippet, that potentially throws the exception.

Below, you can find the code example of this method:

@Test
void throwsTest(){
  Assertions.assertThrows(RuntimeException.class, () -> {
    throw new RuntimeException();
  });
}

Also, as with mentioned methods of assertEquals‘s group, you can provide an optional message of String as third argument.

Assert timeout

When you need to check, that test is finished in specific time, you can use the following method:

assertTimeout(Duration timeout, Executable executable)

This method uses Java Duration API to specify timeframe. It has several handy methods, like ofSeconds(), ofMills() etc.

The code snippet below demonstrates how to use this method:

@Test
void timeoutTest(){
    Duration duration = Duration.ofSeconds(5);
    Assertions.assertTimeout(duration, () -> TimeUnit.SECONDS.sleep(4));
}

Fail test cases

When you need to fail the test case, based on execution logic, use Assertions.fail() method. There is a number of overloaded versions:

  • fail (String message) = Fails a test with the given failure message.
  • fail (String message, Throwable cause) = Fails a test with the given failure message as well as the underlying cause.
  • fail (Throwable cause) = Fails a test with the given underlying cause.

Source code

You can find the source code for this post in this github repository. If you have any questions, regarding this post, don’t hesitate to contact me.

References

  • Sergio Martin. Take Unit Testing to the Next Level With JUnit 5 (2018). DZone, read here
  • Petri Kainulainen. Writing Assertions with JUnit5 Assertion API (2018), read here
  • J Steven Perry. The JUnit5 Jupiter API (2017) IBM Developer, read here