paint-brush
Java Testing Frameworks: Choosing the Right Tool for the Job - JUnit, AssertJ and Hamcrestby@nipunaupeksha
260 reads

Java Testing Frameworks: Choosing the Right Tool for the Job - JUnit, AssertJ and Hamcrest

by Nipuna UpekshaJanuary 8th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Checkout my article on JUnit 5 to write better unit tests and improve your code quality.

Company Mentioned

Mention Thumbnail
featured image - Java Testing Frameworks: Choosing the Right Tool for the Job - JUnit, AssertJ and Hamcrest
Nipuna Upeksha HackerNoon profile picture

Testing is one of the most important aspects when building applications. Java has multiple testing frameworks which help to test different functionalities. But first, we need to have a good idea of why we are writing tests.

Why Tests?

When developing applications, it is possible to miss a certain logic or contain some bugs in the application. We are mainly writing tests to avoid those intricacies. Apart from that, testing also helps you to increase code quality, and it proves that your code is doing what you think it should be doing. Also, it proves that you are following the industry's best practices and other developers can get a good understanding of what your code does.

Test Types

When writing tests it is important to have an idea about the types of tests involved with testing. In this section, we will be looking at the most common test types and get a good grasp of what they mean.

  • Unit Tests/ Unit Testing → These are tests that were written to test the specific sections of your code. These don’t have external dependencies and are intended to run very fast.
  • Integration Tests / Integration Testing → These are tests that were written to test the behaviors between objects and parts of the overall system. These are usually written to cover a larger scope and are slower than unit tests.
  • Functional Tests / Functional Testing → These tests are for testing the running application. Usually, functional touch points are tested in these tests (e.g. calling web services, sending/receiving messages, etc.).

It is important to note that all three types of tests play important roles in software quality. Therefore, it is better to use a combination of the above tests to increase your code quality.

JUnit

When it comes to writing unit tests in Java, the defacto framework that most developers use is JUnit. There are mainly three modules in JUnit which provide different functionalities.

  • JUnit Platform → The foundation for launching testing frameworks on the JVM. This allows tests to be run from console launcher, or build tools such as Maven and Gradle.
  • JUnit Jupiter → Programming model from writing tests and extensions to JUnit.
  • JUnit Vintage → Provides a test engine for running JUnit 3 and JUnit 4 tests.


When using JUnit, we are associating with quite a few annotations. Some of the most common JUnit annotations are shown below.

  • @Test → Marks a method as a test method.
  • @ParameterizedTest → Marks method as a parameterized test.
  • @RepeatedTest → Repeat test N times.
  • @TestFactory → Test factory method for dynamic tests.
  • @TestInstance → Used to configure test instance lifecycle.
  • @TestTemplate → Creates a template to be used by multiple test cases.
  • @DisplayName → Human-friendly name for test.
  • @BeforeEach → Method to run before each test case.
  • @AfterEach → Method to run after each test case.
  • @BeforeAll → Static method to run before all test cases in the current class.
  • @AfterAll → Static method to run after all test cases in the current class.
  • @Nested → Creates a nested test class.
  • @Tag → Declares “tags” for filtering tests.
  • @Disabled → Disable a test or test class.
  • @ExtendWith → Used to register extensions.

We will be looking at these extensively while developing a project.

Getting hands dirty with JUnit

Let’s create a new Maven project to test the above-mentioned annotations and get a good grasp of JUnit. We will not look at how you can install Java or Maven to your system in this article. To grasp how Java and Maven can be effortlessly set up, you can refer to this article. We will be using IntelliJ IDEA as our IDE in this article. If you are not familiar with IntelliJ IDEA, you can use an IDE of your preference for this.

Part 1 - Project Setup

Go to IntelliJ IDEA → File → New → Project to create a new project. Since this is a simple project I have selected the following properties as shown in the image.


1.1 Project Setup


Although, I have created the project for Java 16 (corretto-16) I need to use Java 17 in my project. So how can I fix this? To use Java 17 in your project, simply go to, File → Project Structure and select Java 17 there. But, note that, to use Java 17, you need to have Java 17 SDK installed on your machine.


1.2 Change Java Version


But, only changing this will not work, because you have to update the Java version in pom.xml file as well. To update the Java version, go to pom.xml and change the properties as shown below.

<properties>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
  ...
</properties> 

Now, let’s add JUnit to our project by adding JUnit dependency in the pom.xml file. First, define the JUnit version we are using in the properties section as shown below.

<properties>
  ...
  <junit-platform.version>5.9.2</junit-platform.version>
</properties>

After that, define the dependencies by adding the following to the pom.xml file.

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>${junit-platform.version}</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>${junit-platform.version}</version>
    <scope>test</scope>
  </dependency>
</dependencies>

After that, add the build plugins by adding the following to the pom.xml file.

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.11.0</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.1.2</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <version>2.22.0</version>
    </plugin>
  </plugins>
</build>

After that, you can sync the Maven settings from the IDE and test whether the project is working properly by double-clicking on the test lifecycle dependency (Alternatively you can run mvn clean test in your terminal as well).


1.3 Dependencies added


Since we are creating a small project, we will create a new file in org.nipunaupeksha.junit5 named SimpleCalculator with basic arithmetic operations(+,-,/,*) written there.

package org.nipunaupeksha;

public class SimpleCalculator {
    public double add(double numberA, double numberB) {
        return numberA + numberB;
    }

    public double subtract(double numberA, double numberB) {
        return numberA - numberB;
    }

    public double divide(double numberA, double numberB) {
        return numberA / numberB;
    }

    public double multiply(double numberA, double numberB) {
        return numberA * numberB;
    }

}


After that, you can test this simple program by writing the below code in org.nipunaupeksha.junit5 → Main.java file.

package org.nipunaupeksha;

public class Main {
    public static void main(String[] args) {

        SimpleCalculator simpleCalculator = new SimpleCalculator();

        double resAdd = simpleCalculator.add(5, 10);
        double resSubtract = simpleCalculator.subtract(15, 10);
        double resDivide = simpleCalculator.divide(10, 3);
        double resMultiply = simpleCalculator.multiply(5, 2);

        System.out.println("Addition result: " + resAdd);
        System.out.println("Subtraction result: " + resSubtract);
        System.out.println("Division result: " + resDivide);
        System.out.println("Multiplication result: " + resMultiply);
    }
}


The output of the above code snippet is shown below.


1.4 SimpleCalculator output


Since we have a small project that is working, let’s start writing tests to validate and improve our code.

Part 2 - Writing a Simple Test

When writing test cases, it is important to follow proper file structure. The best practice is to follow the same directory structure you follow for the main section of the code for the test section. Therefore, first, create a package named org.nipunaupeksha inside the test section.


Next, create a file named, SimpleCalculatorTest inside that package.


2.1 Create a new test file named SimpleCalculatorTest


Rather than creating a new package and file, you can easily do this with IntelliJ IDEA as well. To create a new test file, simply click on the class you want to test on (in our case SimpleCalculator ) and press Option + enter in Mac (In Linux and Windows this option can change according to the key bindings(Keymap) you have set on the IntelliJ IDEA). Then it will pop up few options and from those options you can select Create Test to create a new test file.


Now, go to the SimpleCalculatorTest file, and create your first test by adding the following code.

package org.nipunaupeksha;

import org.junit.jupiter.api.Test;

public class SimpleCalculatorTest {

    @Test
    void testAdd() {
    }
}

You can see that, I have used the annotation @Test in the above code snippet. To show the application, that the method you created is only for testing purposes, you have to annotate it with the @Test annotation. Since, we haven’t written any code inside the method, testAdd if we were to run this test, it would pass without any errors. (If you are getting any errors, due to the Java version, fix it in the project settings).


2.2 Test passing without any errors


Now, if we were to test any of the code that we wrote in SimpleCalculator class, we need to bring that object into SimpleCalculatorTest file. But, the issue is, for every test we need to create a new SimpleCalculator object. So how can we fix that?


Since we need to create a new SimpleCalculator object every time we run a test method (e.g. testAdd, testSubtract (not implemented yet)) we are making two mistakes if we are going to directly create that object as shown below.

...
@Test
void testAdd(){
  SimpleCalculator simpleCalulator = new SimpleCalculator();
  ...
}

@Test
void testAdd(){
  SimpleCalculator simpleCalulator = new SimpleCalculator();
  ...
}
...


So how can we avoid that? JUnit gives us an option to run a method every time before a test starts. You can use that option by using the @BeforeEach annotation. So what we can do is update our code as shown below, so that, we do not need to create the same object again and again when we write new tests.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
    }
}

Similar to @BeforeEach we can use the annotation @AfterEach to tear down any of the objects we created or configurations we made. Usually, the method names, setUp and tearDown are used for @BeforeEach and @AfterEach annotations respectively.


Similar to @BeforeEach and @AfterEach annotations, you can use @BeforeAll and @AfterAll annotations. But make note that these annotations can be used only with static methods. Therefore, whatever configurations or objects you are making are done in a static context.

Part 3 - JUnit5 Assertions

When using JUnit you are dealing with assertions most of the time. Assertions simply mean that you are asserting a condition for the test to pass. Let’s say that the add(double numberA, double numberB) is returning a value that we added to a variable named result and we are passing numbers 2,4 to the add() method. Then the output we should get should be 6. We can assert that the method is working correctly with the assertions that JUnit provides(e.g. assertEquals).


Let’s write an assertion to test whether the output we are getting from the add() method is correct or not.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
        assertEquals(6, simpleCalculator.add(2, 4));
    }
}

As you can see we have used the static method assertEquals() to confirm whether the output we are getting from the add() method is correct. Now, we can run it by clicking the gutter icon.


3.1 Test addTest running without any errors


When writing assertions always remember to use the expected value as the first parameter and the code you want to test as the second, actual parameter.


Similar to assertEquals there are quite a few assertions that you can use when using JUnit. You can check all the assertions you can use from here.


We can also pass an additional argument with assertions to provide us with an error message if the assertion fails. This is useful since, when an assertion is failed it usually gives the error trace and does not give a message of what went wrong. Therefore, adding error messages to assertions can help you save a lot of time.


3.2 Assertions with fail message


As you can see, since the assertion has failed it gives you a message saying Wrong result returned. Now we can check the passed parameters and can identify that we have passed 2 and 4 to the add() method instead of 2 and 3. To increase the performance of the message, if it is an expensive message to build, you can use lambda expressions to make sure the fail message is only built when the assertion has failed.


3.3 Assertions with a fail message (using lambda methods)

Part 4 - Grouped Assertions

We can also create grouped assertions with JUnit as well. For example, let’s say we need to confirm all the calculation methods in one go we can use grouped assertions as shown below.

 package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 4), () -> "Wrong result returned.");
    }

    @Test
    void testCalculations(){
        assertAll("Calculations Test",
                ()-> assertEquals(5, simpleCalculator.add(2,3)),
                ()-> assertEquals(1, simpleCalculator.subtract(2,1)),
                ()-> assertEquals(4, simpleCalculator.divide(8,2)),
                ()-> assertEquals(10, simpleCalculator.multiply(2,5)));
    }
}

The first parameter of assertAll is the heading for your assertion group, and the respective properties are what you can test as assertions. Now, if we run that test using the gutter icon, we can see that all of them are running correctly.


4.1 Grouped Assertions without any errors


In addition, we can use fail messages in grouped assertions to find what assertion caused the failure as well.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 4), () -> "Wrong result returned.");
    }

    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5)), "Multiplication failed.");
    }
}

Part 5 - Skipping JUnit Tests

Let’s say you want to skip some of the tests that you have written but you don’t want to delete those tests. In that case, JUnit offers @Disabled annotation to skip that test. For example, if we want to disable the testCalculations() method we wrote, we can simply add the @Disabled annotation for that method.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Disabled
    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

Now, if we run all the tests available in class SimpleCalculatorTest it will only check the testAdd method.


5.1 Skipping JUnit tests


Similarly, you can disable all the test methods in a certain class by adding the annotation @Disabled for the class. For example, if we want to disable all the test methods in SimpleCalculatorTest class we can use the following code snippet.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Disabled
public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

And we can add a message to the @Disabled annotation as well.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Disabled(value = "Testing @Disabled Annotation")
public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

Part 6 - Giving Names to Tests

When we are writing and testing the tests we can only see the test method name like, testAdd or testCalculations. But if we use more human-friendly names for our tests, it helps other developers to understand the tests we wrote and improve our code quality as well. To do that, we can use @DisplayName annotation.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @DisplayName("Testing the add() method in SimpleCalculator")
    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Disabled
    @DisplayName("Testing all the arithmetic operations in SimpleCalculator")
    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

Now, we can see the display names of our tests when we run them.


6.1 Display names of the tests

Part 7 - Testing for Expected Exceptions

We know that if you can’t divide any number by 0. So we need to alter our SimpleCalculator code a little bit to throw an ArithmeticException when the numberB is 0. Let’s do that first.

package org.nipunaupeksha;

public class SimpleCalculator {
    public double add(double numberA, double numberB) {
        return numberA + numberB;
    }

    public double subtract(double numberA, double numberB) {
        return numberA - numberB;
    }

    public double divide(double numberA, double numberB) {
        if(numberB == 0) throw new ArithmeticException("numberB cannot be zero");
        return numberA / numberB;
    }

    public double multiply(double numberA, double numberB) {
        return numberA * numberB;
    }

}

Now we have altered our code, how can we test that? Testing for expected exceptions in JUnit is quite different from simple assertions since we have to use assertThrows for that. Let’s create a new test named testDivideException for this scenario to check whether an ArithmeticException is thrown.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @DisplayName("Testing the add() method in SimpleCalculator")
    @Test
    void testAdd() {
        // assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Test
    void testDivideException() {
        assertThrows(ArithmeticException.class, () -> simpleCalculator.divide(10, 0));
    }

    @Disabled
    @DisplayName("Testing all the arithmetic operations in SimpleCalculator")
    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

Now, if we run the test, we can see that it is marked green to indicate the test is successful.


7.1 Testing expected exceptions

Part 8 - JUnit Assumptions

JUnit assumptions are conditional statements you write to check whether your assumption on the code is correct or not. Unlike assertions, they don’t fail the test if the expected value is not similar to the actual value. Instead, they will be ignored if the expected value is not similar to the actual value. Let’s check how you can write the assumptions using JUnit.

...
@Test
void testAddAssumption(){
  assumeTrue(simpleCalculator.add(2,4) == 5);
}
...

Now, you can see that the assumption we made is false since 2+4 is not equal to 5. If we run this, our test testAddAssumption will not fail, but it will get ignored.


8.1 JUnit Assumptions


Assumptions are really important whenever it does not make sense to continue the execution of a given test method — for example, if the test depends on something that does not exist in the current runtime environment.

Part 9 - Conditional Test Execution with JUnit

We can perform conditional testing with JUnit as well. Assume you have a method, that you need to test on Mac and Windows. You can use conditional testing to do that. Similarly, if you want to test your method with different Java versions, you can do that with conditional testing as well. A few examples of using, conditional testing have been given below. You can find more about conditional testing from here.

...
@DisplayName("Testing the add() method in MacOs")
@EnabledOnOs(OS.MAC)
@Test
void testAddOnMacOs(){
  assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
}

@DisplayName("Testing the add() method in Windows")
@EnabledOnOs(OS.WINDOWS)
@Test
void testAddOnWindows(){
  assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
}

@DisplayName("Testing the add() method in Java 8")
@EnabledOnJre(JRE.JAVA_8)
@Test
void testAddOnJava8(){
  assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
}

@DisplayName("Testing the add() method in Java 17")
@EnabledOnJre(JRE.JAVA_17)
@Test
void testAddOnJava17(){
  assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
}
...

Now, if we run the test class SimpleCalculatorTest you can see that, it does not run testAddOnWindows and testAddOnJava8 since I am using MacOS and Java 17 in this project.


9.1 Conditional Testing


In addition to the aforementioned annotations, you can use @EnabledIfEnvironmentVariable (e.g. @EnabledIfEnvironmentVariable(named=”USER”, matches=”nipunaupeksha”))annotation to conditionally run your tests based on your environment variables.

Part 10 - Using AssertJ with JUnit

AssertJ is an alternate asserting library that you can use along with JUnit. In this section, we will look at some of the features of AssertJ that you can use with JUnit. If you need more information on AssertJ you can always use the official documentation to check them out.


To use AssertJ, we need to add that to the pom.xml file’s dependency list.

...
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.24.2</version>
  <scope>test</scope>
</dependency>
...


Now, let’s try using AssertJ to write a new test case for our subtract() method in SimpleCalculator.

import static org.assertj.core.api.Assertions.assertThat;
...
@DisplayName("Testing the subtract() method in SimpleCalculator using AssertJ")
@Test
void testSubtract(){
  assertThat(simpleCalculator.subtract(10, 5)).isEqualTo(5);
}
...

As you can see, with a simple assertThat method, it provides several methods that we can chain to use with the assertions. This helps the code to be simpler since we are only importing one static method and we can chain any assertion we need to that assertThat method(e.g. isEqualTo(), isNotEqualTo()).


10.1 Using AssertJ for Assertions

Part 11 - Using Hamcrest with JUnit

Hamcrest is another library that you can use with JUnit for more versatile assertions. Let’s check how you can use Hamcrest with JUnit by adding it to the pom.xml file’s dependency list.

...
<dependency>
  <groupId>org.hamcrest</groupId>
  <artifactId>hamcrest-library</artifactId>
  <version>2.2</version>
  <scope>test</scope>
</dependency>
...

Let’s check the multiply() method of SimpleCalculator with Hamcrest. Since we have to use assertThat() in Hamcrest as well, comment out the AssertJ imports and the test method, testSubtract() .

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
...
@DisplayName("Testing the multiply() method in SimpleCalculator using Hamcrest")
@Test
void testMultiply() {
  assertThat(simpleCalculator.multiply(2,5), is(10.0));
}
...

11.1 Using Hamcrest Assertions

Part 12 - JUnit Tags

JUnit tags allow you to identify the tests and put those tests into a group. You can define tests at the class or method level. Depending on the test files and methods, you can group them properly with class-level or method-level tags. Let’s use tags to group our tests in the SimpleCalculatorTest file.

package org.nipunaupeksha;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;

//import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

public class SimpleCalculatorTest {

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in SimpleCalculator")
    @Test
    void testAdd() {
//        assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in SimpleCalculator as an assumption ")
    @Test
    void testAddAssumption(){
        assumeTrue(simpleCalculator.add(2,4) == 5);
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in MacOs")
    @EnabledOnOs(OS.MAC)
    @Test
    void testAddOnMacOs(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Windows")
    @EnabledOnOs(OS.WINDOWS)
    @Test
    void testAddOnWindows(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Java 8")
    @EnabledOnJre(JRE.JAVA_8)
    @Test
    void testAddOnJava8(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Java 17")
    @EnabledOnJre(JRE.JAVA_17)
    @Test
    void testAddOnJava17(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

//    @DisplayName("Testing the subtract() method in SimpleCalculator using AssertJ")
//    @Test
//    void testSubtract(){
//        assertThat(simpleCalculator.subtract(10, 5)).isEqualTo(5);
//    }

    @Tag("Division")
    @DisplayName("Testing the ArithmeticException when you divide any number by zero")
    @Test
    void testDivideException() {
        assertThrows(ArithmeticException.class, () -> simpleCalculator.divide(10, 0));
    }

    @Tag("Multiplication")
    @DisplayName("Testing the multiply() method in SimpleCalculator using Hamcrest")
    @Test
    void testMultiply() {
        assertThat(simpleCalculator.multiply(2,5), is(10.0));
    }

    @Tag("All")
    @Disabled
    @DisplayName("Testing all the arithmetic operations in SimpleCalculator")
    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

So, now if we want to run only the Addition tests, we can configure it in the IntelliJ IDEA by going to the top toolbar and clicking on the Edit Configurations.


12.1 Setting up tag configurations


Click on the + symbol to add a new JUnit configuration and give it a name. Then select the resource to Tags and give the tag name there (in our case Addition).


12.2 JUnit Configurations for the tag, Addition


Now, if you run that configuration, only the tests marked with @Tag(“Addition”) will get executed.


12.3 Running only the tests marked with the Addition tag

Part 13 - Nested Tests

You can write nested tests by defining another class within the test class. Let’s create a new file named, SimpleCalculatorNestedTest to test the nested tests.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

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

@DisplayName("SimpleCalculator")
public class SimpleCalculatorNestedTest {
    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp(){
        simpleCalculator = new SimpleCalculator();
    }

    @DisplayName("Addition")
    @Nested
    class NestedAddition{

        @DisplayName("Check adding two numbers is correct.")
        @Test
        void testAddTwoNumbers(){
            assertEquals(10, simpleCalculator.add(8,2));
        }
    }

    @DisplayName("Division")
    @Nested
    class NestedDivision{

        @DisplayName("Check dividing two numbers is correct.")
        @Test
        void testAddTwoNumbers(){
            assertEquals(4, simpleCalculator.divide(8,2));
        }
    }
}


Now, if you run this, you can see, the test outputs are grouped due to the usage of @Nested annotation.


13.1 Nested Annotation Usage

Part 14 - JUnit Test Interfaces

Since we have two test files named SimpleCalculatorTest and SimpleCalculatorNestedTest, we can use @Tag annotation to group them into something like SimpleCalculator(@Tag(“SimpleCalculator”)). Also, we can use test interfaces for that as well. Let’s create a new interface, ISimpleCalculator with the tag SimpleCalculator and use them in the aforementioned classes, so that they can be used with the tag, SimpleCalculator.

package org.nipunaupeksha;

import org.junit.jupiter.api.Tag;

@Tag("SimpleCalculator")
public interface ISimpleCalculator {
}


package org.nipunaupeksha;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;

//import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

public class SimpleCalculatorTest implements ISimpleCalculator{

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in SimpleCalculator")
    @Test
    void testAdd() {
//        assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in SimpleCalculator as an assumption ")
    @Test
    void testAddAssumption(){
        assumeTrue(simpleCalculator.add(2,4) == 5);
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in MacOs")
    @EnabledOnOs(OS.MAC)
    @Test
    void testAddOnMacOs(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Windows")
    @EnabledOnOs(OS.WINDOWS)
    @Test
    void testAddOnWindows(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Java 8")
    @EnabledOnJre(JRE.JAVA_8)
    @Test
    void testAddOnJava8(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Java 17")
    @EnabledOnJre(JRE.JAVA_17)
    @Test
    void testAddOnJava17(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

//    @DisplayName("Testing the subtract() method in SimpleCalculator using AssertJ")
//    @Test
//    void testSubtract(){
//        assertThat(simpleCalculator.subtract(10, 5)).isEqualTo(5);
//    }

    @Tag("Division")
    @DisplayName("Testing the ArithmeticException when you divide any number by zero")
    @Test
    void testDivideException() {
        assertThrows(ArithmeticException.class, () -> simpleCalculator.divide(10, 0));
    }

    @Tag("Multiplication")
    @DisplayName("Testing the multiply() method in SimpleCalculator using Hamcrest")
    @Test
    void testMultiply() {
        assertThat(simpleCalculator.multiply(2,5), is(10.0));
    }

    @Tag("All")
    @Disabled
    @DisplayName("Testing all the arithmetic operations in SimpleCalculator")
    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}


package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

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

@DisplayName("SimpleCalculator")
public class SimpleCalculatorNestedTest implements ISimpleCalculator{
    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp(){
        simpleCalculator = new SimpleCalculator();
    }

    @DisplayName("Addition")
    @Nested
    class NestedAddition{

        @DisplayName("Check adding two numbers is correct.")
        @Test
        void testAddTwoNumbers(){
            assertEquals(10, simpleCalculator.add(8,2));
        }
    }

    @DisplayName("Division")
    @Nested
    class NestedDivision{

        @DisplayName("Check dividing two numbers is correct.")
        @Test
        void testAddTwoNumbers(){
            assertEquals(4, simpleCalculator.divide(8,2));
        }
    }
}


Now, if we want to create a JUnit configuration to execute the tag SimpleCalculator it will execute both SimpleCalculatorTest and SimpleCalculatorNestedTest files. Also, we can define default methods in the interface as well. For instance, if we want to output something like SimpleCalculator tests are running we can implement it as shown below. We can use this more extensively to avoid code repetition.

package org.nipunaupeksha;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInstance;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tag("SimpleCalculator")
public interface ISimpleCalculator {
    @BeforeAll
    default void beforeAll(){
        System.out.println("SimpleCalculator tests are running");
    }
}

Part 15 - Repeated Tests

If we want to repeat a test multiple times, we can use the annotation @RepeatedTest. Here, the number of repetitions you need is passed as a parameter.

...
@Tag("Addition")
@DisplayName("Repeatedly testing the add() method")
@RepeatedTest(10)
void testAddRepeat(){
  assertEquals(5, simpleCalculator.add(2,3), ()-> "Wrong result returned,");
}
...

We can further configure this by changing the parameters passed to @RepeatedTest(). For instance, if I need the display names as Repeated Test: 1 of 10 I can configure it as shown below.

...
@Tag("Addition")
@DisplayName("Repeated Test")
@RepeatedTest(value= 10, name= "{displayName}: {currentRepetition} of {totalRepetitions}")
void testAddRepeat(){
  assertEquals(5, simpleCalculator.add(2,3), ()-> "Wrong result returned,");
}
...

15.1 Repeated Testing

Part 16 - JUnit Parameterized Tests

To use parameterized tests in JUnit we need to add another dependency to the dependency list in the pom.xml file. That is junit-jupiter-params . After adding it, you can write parameterized tests with JUnit.

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-params</artifactId>
  <version>${junit-platform.version}</version>
  <scope>test</scope>
</dependency>

Let’s write our first parameterized test for multiply() method.

...
@ParameterizedTest
@ValueSource(doubles={5, 10})
void testMultiplyWithValueSource(double val){
  assertEquals(val, simpleCalculator.multiply(val, 1));
}
...

Now, if you run this test, the provided values 5,10 will be passed as arguments to this method testMultiplyWithValueSource


16.1 Parameterized Test with a Value Source


If we need to give human-friendly names to our parameterized tests, we can do it by using the @DisplayName annotation with the @ParameterizedTest annotation as shown below.

...
@DisplayName("Value source multiplication test")
@ParameterizedTest(name="{displayName} - [{index}] {arguments}")
@ValueSource(doubles={5, 10})
void testMultiplyWithValueSource(double val){
  assertEquals(val, simpleCalculator.multiply(val, 1));
}
...

This will use the placeholders mentioned in the @ParameterizedTest annotation and provide us human-friendly display names.


16.2 Better display names for parameterized tests


One thing to remember is although we have used the @ValueSource annotation to show an example of using parameterized tests, you can use annotations like @EnumSource, @CsvSource , @CsvFileSource, @MethodSource, @ArgumentsSource depending on the parameters you want to pass.

Part 17 - JUnit Extensions

We can use different JUnit extensions with our test files. For example, you can get the TimingExtension file from here, and add it to our project to be used with our test files. To do that, create a new package named, junitextensions and add the following code there.

package org.nipunaupeksha.junitextensions;

import java.lang.reflect.Method;
import java.util.logging.Logger;

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

/**
 * source -  https://github.com/junit-team/junit5/blob/main/documentation/src/test/java/example/timing/TimingExtension.java
 */

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());

    private static final String START_TIME = "start time";

    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        getStore(context).put(START_TIME, System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        Method testMethod = context.getRequiredTestMethod();
        long startTime = getStore(context).remove(START_TIME, long.class);
        long duration = System.currentTimeMillis() - startTime;

        logger.info(() ->
                String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
    }

    private Store getStore(ExtensionContext context) {
        return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
    }
}

Now, go to SimpleCalculatorTest file and use the annotation @ExtendWith(TimingExtension.class) to use the TimingExtension with that class.

package org.nipunaupeksha;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.nipunaupeksha.junitextensions.TimingExtension;

//import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

@ExtendWith(TimingExtension.class)
public class SimpleCalculatorTest implements ISimpleCalculator{

    private SimpleCalculator simpleCalculator;

    @BeforeEach
    void setUp() {
        simpleCalculator = new SimpleCalculator();
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in SimpleCalculator")
    @Test
    void testAdd() {
//        assertEquals(5, simpleCalculator.add(2, 4), "Wrong result returned.");
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in SimpleCalculator as an assumption ")
    @Test
    void testAddAssumption(){
        assumeTrue(simpleCalculator.add(2,4) == 5);
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in MacOs")
    @EnabledOnOs(OS.MAC)
    @Test
    void testAddOnMacOs(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Windows")
    @EnabledOnOs(OS.WINDOWS)
    @Test
    void testAddOnWindows(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Java 8")
    @EnabledOnJre(JRE.JAVA_8)
    @Test
    void testAddOnJava8(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Testing the add() method in Java 17")
    @EnabledOnJre(JRE.JAVA_17)
    @Test
    void testAddOnJava17(){
        assertEquals(5, simpleCalculator.add(2, 3), () -> "Wrong result returned.");
    }

    @Tag("Addition")
    @DisplayName("Repeated Test")
    @RepeatedTest(value= 10, name= "{displayName}: {currentRepetition} of {totalRepetitions}")
    void testAddRepeat(){
        assertEquals(5, simpleCalculator.add(2,3), ()-> "Wrong result returned,");
    }

//    @DisplayName("Testing the subtract() method in SimpleCalculator using AssertJ")
//    @Test
//    void testSubtract(){
//        assertThat(simpleCalculator.subtract(10, 5)).isEqualTo(5);
//    }

    @Tag("Division")
    @DisplayName("Testing the ArithmeticException when you divide any number by zero")
    @Test
    void testDivideException() {
        assertThrows(ArithmeticException.class, () -> simpleCalculator.divide(10, 0));
    }

    @Tag("Multiplication")
    @DisplayName("Testing the multiply() method in SimpleCalculator using Hamcrest")
    @Test
    void testMultiply() {
        assertThat(simpleCalculator.multiply(2,5), is(10.0));
    }

    @DisplayName("Value source multiplication test")
    @ParameterizedTest(name="{displayName} - [{index}] {arguments}")
    @ValueSource(doubles={5, 10})
    void testMultiplyWithValueSource(double val){
        assertEquals(val, simpleCalculator.multiply(val, 1));
    }

    @Tag("All")
    @Disabled
    @DisplayName("Testing all the arithmetic operations in SimpleCalculator")
    @Test
    void testCalculations() {
        assertAll("Calculations Test",
                () -> assertEquals(5, simpleCalculator.add(2, 3), "Addition failed."),
                () -> assertEquals(1, simpleCalculator.subtract(2, 1), "Subtraction failed."),
                () -> assertEquals(4, simpleCalculator.divide(8, 2), "Division failed."),
                () -> assertEquals(10, simpleCalculator.multiply(2, 5), "Multiplication failed."));
    }
}

Now, if you run the tests in this file, you can see the timings for each of the tests because you have extended your class with TimingExtension. You can find more JUnit extensions from their documentation and create your own custom extensions that can be integrated to your test classes as shown above.


17.1 JUnit Extensions

Part 18 - Test Execution

Now, we have covered lots of topics related to JUnit in this article, but we haven’t discussed how you can check whether you have written enough tests to cover all of your codes. IntelliJ IDEA provides a way to find out the percentage of code coverage in your project. To find out the code coverage in your project, right-click on the java package in test section and select Run all tests with coverage option.


18.1 Run Tests with Coverage


After that, all the tests will be run one more time, and you can see the percentages of the code coverage of each file you have created.


18.2 Code Coverage Percentages


It is better if you can get 100% code coverage. But it is better to have at least 80% of code coverage.

Part 19 - Maven Surefire Test Reporting

We can check the summary of our tests as report, using the Maven surefire plugin. To get that reporting capability we need to update our pom.xml file a bit.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.nipunaupeksha</groupId>
    <artifactId>junit5</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit-platform.version>5.9.2</junit-platform.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit-platform.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit-platform.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit-platform.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.24.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <version>2.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.22.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-site-plugin</artifactId>
                <version>3.12.1</version>
            </plugin>
        </plugins>
    </build>
    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-report-plugin</artifactId>
                <version>3.1.2</version>
            </plugin>
        </plugins>
    </reporting>
</project>

As you can identify, we need to add maven-site-plugin and maven-surefire-report-plugin to generate a test summary report.


Double click on site lifecycle goal from the Maven tab in IntelliJ IDEA or run mvn clean site to generate the test report.


19.1 Running site Lifecycle Goal


Next, open the target directory and find the index.html file located in the site directory. Open it with your browser to see the surefire report.


19.2 Open index.html file in Browser


Now, if you click on Project Reports you will be able to see the surefire test report.


19.3 Surefire Test Report

Part 20 - From JUnit 4 to JUnit 5

When you are migrating from JUnit 4 to JUnit 5, it is important to know that some of the annotations have been changed in JUnit 5. Some of the most important, changed annotations are shown below.

  • JUnit 4 @Before → JUnit 5 @BeforeEach
  • JUnit 4 @After → JUnit 5 @AfterEach
  • Junit 4 @BeforeClass → JUnit 5 @BeforeAll
  • JUnit 4 @AfterClass → JUnit 5 @AfterAll
  • JUnit 4 @Ignored → JUnit 5 @Disabled
  • JUnit 4 @Category → JUnit 5 @Tag

You can find other changes by checking the JUnit 5 official documentation.


Since, I am using IntelliJ IDEA, I have always referenced the Migrating from JUnit 4 to JUnit 5 **by Helen Scott, whenever I need to migrate to JUnit 5 from JUnit 4. Make sure to update your pom.xml file accordingly if you are migrating to JUnit 5 from JUnit 4.

Conclusion

So, this is it! This is how you can use JUnit with other assertion tools like AssertJ or Hamcrest for Unit tests. Let’s talk about how you can use other frameworks like Mockito and Spring Testing Framework some other time. You can find the project we have used for this article from here. If you have thoughts or suggestions for me always feel free to let me know.