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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
Since we have a small project that is working, let’s start writing tests to validate and improve our code.
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.
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).
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.
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.
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.
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.
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.
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.");
}
}
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.
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."));
}
}
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.
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.
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.
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.
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.
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.
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()
).
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));
}
...
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
.
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
).
Now, if you run that configuration, only the tests marked with @Tag(“Addition”)
will get executed.
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.
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");
}
}
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,");
}
...
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
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.
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.
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.
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.
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.
It is better if you can get 100% code coverage. But it is better to have at least 80% of code coverage.
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.
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.
Now, if you click on Project Reports
you will be able to see the surefire test report.
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.
@Before
→ JUnit 5 @BeforeEach
@After
→ JUnit 5 @AfterEach
@BeforeClass
→ JUnit 5 @BeforeAll
@AfterClass
→ JUnit 5 @AfterAll
@Ignored
→ JUnit 5 @Disabled
@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.
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.