Notes about JUnit 5

Notes about JUnit 5

JUnit 5 offers a useful framework for developing unit tests in the Java language. It is an example of a Test Driver - something that can take a set of test cases and run them and collect the results.  Whilst JUnit name suggests that this is intended for unit tests, it has wider applicability, as discussed at the end of the page.

This page covers issues that are relevant when learning to use JUnit to write unit tests.

Example Test

The following code contains an example of a test in JUnit.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class ExampleTests { 

    @Test
    public void shouldTestAddition() { 
        int actualResult = 5 + 5;
        assertEquals(10, actualResult);
    }

}

There are a few things to explain:

  • The use of the @Test annotation.
  • The method name.
  • The assert statement.
  • The import static statement.

The @Test annotation

Every method that you want to run using JUnit should have the @Test annotation added before the method.

@Test
public void shouldHaveATestName() { 
    // body of the test here
}

Without this, JUnit will not recognise the method as a test. If a test method is not run, then you should check if it has the @Test annotation. It is a common mistake to forget to add it when you begin to use JUnit.

The method name

Each method represents an individual test case. If you have done much testing, then you will know that a test case should have an identifier that is unique for the list of test cases that you are building for the system.

The method name provides us with a unique identifier for our test case. Actually, given this is a member of a class, which is a member of a Java package, then we have a unique identifier for the test case within our project.

There are different naming conventions used. I see some people following a format where each test starts with the word test. For example:

@Test
public void testConstructorSetsDefaultNameValues() { 
   // test statements
}

Another common format is to use the word should at the start of the tests. Whilst subtle, this can make the test name feel more like a statement of what is being checked. For example:

@Test
public void shouldCreateInstanceWithDefaultValues() { 
   // test statements
}

I have also seen some examples where the test name starts with the main issue, e.g. shouldCreateInstance, which is then followed by an underscore to qualify the focus of the test being run. For example:

@Test
public void shouldCreateInstance_WithNoParameters() { 
    // test statements
}

@Test
public void shouldCreateInstance_WithNameAndAddressSpecified() { 
    // test statements
}

The actual format is a decision for you and your development team. The important issue is that you have descriptive names and that you are consistent in your choice of naming.

The assert statement

All tests need to make an assertion - what should be true if the test is to pass. If the assertion evaluates to false, then the test should fail.

JUnit provides several assertion statements that allow you to check some condition. These include assertEquals(), assertNull, assertNotSame and assertThrows.

If a test method is going to be useful, it needs at least one assertion. Without any assertion, what are you actually testing?

For each of the assertations, there is a check whether an expected value matches the specified value. For assertEquals, this means the following:

int someCalculation = 10 * 10; 
assertEquals(100, someCalculation);

The first parameter is the expected value. I will normally insert that as a literal value to make it clear what the expected value should be. The second parameter is the actual value, from whatever we are testing.

A method such as assertNull does not have two values. It just tests the expected value to determine if it is null. For example:

String someValue = "a value";
assertNull(someValue);

The assert methods are overloaded to handle the different combinations of types (int, double, String, char, Object, etc.) that are possible in the language.

Each of the assertions can be written with a String description that is shown if the test fails. For assertEquals, this would mean:

int someCalculation = 7 * 10;
assertEquals("Incorrect value for calculation", 70, someCalculation);

Most of the assertions start with the word assert. There is a further assertation statement called fail. This is used to indicate that the test should fail immediately. It can be used to remind you that a test has not been implemented, such as the following example:

@Test 
public void test() { 
    fail("Add test implementation");
}

Some IDEs, e.g. Netbeans, have added such template code when you create a new JUnit test class.

You can also use the fail in a situation such as the following:

@Test 
public void shouldThrowException() { 
   try { 
      ExampleObject object = new ExampleObject();
      object.methodThatShouldThrowAnExceptionWhenPassedNull(null);
      fail("Exception not thrown");
   }
   catch(IllegalArgumentException e) { 

   }
}

If the IllegalArgumentException is thrown, then the exception will catch it. No assertion is run, so nothing will fail the test. Therefore, the test will pass. If the exception is not thrown, the fail assertion will run and the test will fail.

That said, there are better ways to handle exceptions with the assertThrows method, which is an improvement in JUnit 5 on how to handle errors in previous JUnit versions.

See the JUnit 5 documentation for more details about the assertions - there are lots of other features to explore.

Importing the Assertion Statements

When using JUnit 5, you will normally see the following import statement at the top of your test class files.

import static org.junit.jupiter.api.Assertions.*;

This line contains the word static with the import statement. You might not have seen this part of the Java language before.

All of the assertion methods, e.g. assertEquals are defined in the class org.junit.jupiter.api.Assertions. The assertion methods are static methods on that class.

The import static statement allows us to access these assertion methods without having to type Assertions when we use them. So, instead of writing Assertions.assertEquals(expectedValue, actualValue) we can type assertEquals(expectedValue, actualValue). That makes our tests easier to read.

Testing for exceptions

As mentioned above, JUnit 5has introduced a new way to write tests for exceptions. This is a change from JUnit 4. An example is shown below:

@Test
public void shouldSetNegativeNumberAndThrowException() { 
    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
        Address address = new Address(); 
        address.setNumber(-1);
    });

    assertEquals("Must be 0 or more", exceptiong.getMessage());
}

The assertThrows method has been added. It takes two parameters:

  • The first one is the expected Exception class that should be thrown. Note, we need to add .class to the end of the exception name so that it is the correct type for compilation.
  • The second parameter is a lambda function that takes no parameters. Inside that function, you write the code that should cause the exception to be thrown.

If the exception is thrown, then it is available in your test method as the result from assertThrows. This means we can then check things about the exception, such as whether it has the correct message.

Testing execution with set time

Another new feature in JUnit 5 is the ability to assert that a task will complete within a certain time. The following example is based on the JUnit user guide:

@Test
public void shouldNotExceedTimeoutOfTwoMinutes() { 

   assertTimeout(ofMinutes(2), () -> { 
      // some statements that should complete within the specified time.
   });

}

See the JUnit 5 User Guide for more information.

JUnit, the driver for test automation

JUnit was developed to provide a framework to make it easy to write and run unit tests. However, it is also capable of running other tests, such as integration tests or GUI tests. It is a test driver, which is something that will take a set of test cases and run the tests and collect and report the results.

If you can write a set of steps in Java for what you want to test and use assertions to check if those steps were successful, then JUnit 5 can run those steps. Those steps could be for checking integration issues, or running some performance tests or GUI tests.