Java Unit Testing with JUnit Example
This is an article for Java Unit Testing with JUnit Example.
You can also check this tutorial in the following video:
1. Introduction
Java unit testing is a software testing where methods and classes are tested. JUnit is a unit testing framework for the Java programming language which provides a way to test the application as many as you want. Unit testing usually includes the following steps:
- define a test
- create an instance of the testing class
- prepare the test data
- execute a test
- verify the testing results
- report the testing results
JUnit supports step 1 via @Test
annotation, step 4 via @RunWith
annotation, and step 5 via assertion API. In this example, I will create a multi-module maven project to demonstrate how to utilize the JUnit framework to create a test class.
2. Technologies Used
The example code in this article was built and run using:
- Java 11
- Maven 3.3.9
- Eclipse Oxygen
- JUnit (4 and 5)
3. Maven Multi-Modules Project
JUnit 5 was released in 2017. It is not backwards compatible with JUnit 4 which released in 2006. In this step, I will demonstrate both JUnit 4 and JUnit 5 in a three-module Maven project:
common
– includes a main class –SomeClass
.JUnit4-demo
– testsSomeClass
with JUnit 4.JUnit5-demo
– testsSomeClass
with JUnit 5.
3.1 Parent POM
Parent pom.xml includes three modules and two common build plug-ins:
maven-compiler-plugin
– defines the Java 11 for the compilermaven-surefire-plugin
– defines the JUnit report plug-in
pom.xml
<?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>jcg.zheng.demo</groupId> <artifactId>junit-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>junit-demo</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <description>parent project for junit demo</description> <modules> <module>common</module> <module>junit4-demo</module> <module>junit5-demo</module> </modules> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <release>11</release> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M3</version> </plugin> </plugins> </build> </project>
Execute mvn clean install command and capture the output here:
[INFO] Reactor Summary for junit-demo 0.0.1-SNAPSHOT: [INFO] [INFO] junit-demo ......................................... SUCCESS [ 2.287 s] [INFO] comon .............................................. SUCCESS [ 10.295 s] [INFO] junit4-demo ........................................ SUCCESS [ 6.631 s] [INFO] junit5-demo ........................................ SUCCESS [ 6.191 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 26.052 s [INFO] Finished at: 2020-03-30T20:46:54-05:00 [INFO] ------------------------------------------------------------------------
4. Common Module
In this step, I will create a common module which contains a main class. The main class will be tested at both JUnit 4 and JUnit 5 at its respective module.
4.1 POM
The common
module’s pom.xml is defined as the following:
pom.xml
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>jcg.zheng.demo</groupId> <artifactId>junit-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>common</artifactId> <name>comon</name> <url>http://maven.apache.org</url> </project>
4.2 SomeClass
In this step, I will create SomeClass
which has the following methods:
doubleANumber
– return an integer number by multiplying two.returnABoolean
– return aboolean
value based on the input string value.voidFoo
– does not return anything and throws an exception when receiving a bad argument.
SomeClass.java
package jcg.zheng.demo; public class SomeClass { public int doubleANumber(int num) { return num * 2; } public boolean returnABoolean(String inputData) { if ("Save".equalsIgnoreCase(inputData)) { return true; } else { return false; } } public void voidFoo(String inputData) { if ("Ok".equalsIgnoreCase(inputData)) { System.out.println("doing something.");; } else { throw new IllegalArgumentException("Bad argument:" + inputData); } } }
5. JUnit 4 Module
JUnit 4 was first released in 2006. It only has one jar and requires JDK 5 or higher version.
5.1 POM
The JUnit4-demo
module’s pom.xml
and depends on JUnit 4 and the common
module.
In this step, I will create a JUnit 4 test class to test SomeClass
.
pom.xml
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>jcg.zheng.demo</groupId> <artifactId>junit-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>junit4-demo</artifactId> <name>junit4-demo</name> <url>http://maven.apache.org</url> <properties> <junit.version>4.12</junit.version> </properties> <dependencies> <dependency> <groupId>jcg.zheng.demo</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> </project>
5.2 SomeClassTest
In this step, I will create a SomeClassTest
class in JUnit 4.
- Define a test with
@org.junit.Test
- Print out a test name with a
@org.junit.Rule
on aorg.junit.rules.TestName
class - Setup the test before each tests with
@org.junit.Before
- Ignore a test with
@org.junit.Ignore
- Set a test with a timeout limitation
- Set a test with an expected exception
- Verify the testing result with the expected value with a static class
org.junit.Assert
SomeClassTest.java
package jcg.zheng.demo.junit4; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import jcg.zheng.demo.SomeClass; public class SomeClassTest { private SomeClass classUnderTest = new SomeClass(); @Rule public TestName testName = new TestName(); @Before public void setup() { classUnderTest = new SomeClass(); System.out.println("Start " + testName.getMethodName()); } @Test public void test_doubleANumber() { assertEquals(6, classUnderTest.doubleANumber(3)); } @Ignore public void test_not_executed() { fail("It should not executed"); } @Test public void test_returnBooleanFoo_false() { boolean shouldReturnFalse = classUnderTest.returnABoolean("NA"); assertFalse(shouldReturnFalse); } @Test public void test_returnBooleanFoo_true() { boolean shouldReturnTrue = classUnderTest.returnABoolean("Save"); assertTrue(shouldReturnTrue); } @Test public void test_voidFoo() throws IllegalAccessException { try { classUnderTest.voidFoo("OK"); } catch (Exception e) { fail("Should not throw exception"); } } @Test(expected = IllegalArgumentException.class) public void test_voidFoo_exception() throws IllegalAccessException { classUnderTest.voidFoo("NA"); } @Test(timeout = 1) public void test_timeout() { classUnderTest.doubleANumber(9999); } }
- Line 20, 26 – the
TestName
instance marked by@Rule
can access the test name. - Line 23 – the method marked with
@Before
will be invoked before executing each test. - Line 29 –
@Test
marks a method as a test. It will be executed by the JUnit default runner. - Line 34 – JUnit runner will ignore test tests which marks with
@Ignore
. - Line 31, 42, 48 – invokes
assertFalse
,assertTrue
,assertEquals
to verify the test results to the expected value. - Line 60 – catch the expected exception.
- Line 65 – set up the timeout limit.
Output
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running jcg.zheng.demo.junit4.SomeClassTest Start test_voidFoo doing something. Start test_returnBooleanFoo_false Start test_voidFoo_exception Start test_doubleANumber Start test_timeout Start test_returnBooleanFoo_true [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.214 s - in jcg.zheng.demo.junit4.SomeClassTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
6. JUnit 5 Module
JUnit 5 was first released in 2017. It requires JDK 8 or higher. It includes a collection of three sub-projects: JUnit Jupiter, JUnit Platform, and JUnit Vintage.
6.1 POM
The JUnit5-demo
module’s pom.xml
depends on JUnit 5 and common modules. Please note that it includes two of JUnit 5 modules: junit-jupiter-engine
and junit-jupiter-api
.
In this step, I will create a JUnit 5 test class to test SomeClass
.
pom.xml
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>jcg.zheng.demo</groupId> <artifactId>junit-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>junit5-demo</artifactId> <name>junit5-demo</name> <url>http://maven.apache.org</url> <properties> <junit-jupiter.version>5.5.2</junit-jupiter.version> </properties> <dependencies> <dependency> <groupId>jcg.zheng.demo</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> </dependencies> </project>
6.2 SomeClassTest
In this step, I will create a SomeClassTest
class in JUnit 5.
- Define a test with
@org.junit.jupiter.api.Test
- Define a display name with
@org.junit.jupiter.api.DisplayName
- Print out a test name from
@org.junit.jupiter.api.TestInfo
- Setup the test before each tests with
@org.junit.jupiter.api.BeforeEach
- Ignore a test with
@org.junit.jupiter.api.Disabled
- Set a test with the
org.junit.jupiter.api.assertTimeout
method - Catch an exception with the
org.junit.jupiter.api.assertThrow
method - Verify the testing result with the expected value with a
static
class:org.junit.jupiter.api.Assertions
SomeClassTest.java
package jcg.zheng.demo.junit5; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestReporter; import jcg.zheng.demo.SomeClass; public class SomeClassTest { private SomeClass classUnderTest; private TestInfo testInfo; private TestReporter testReporter; @BeforeEach public void setup(TestInfo testInfo, TestReporter terstReporter ) { this.testInfo = testInfo; this.testReporter = terstReporter; classUnderTest = new SomeClass(); } @RepeatedTest(5) public void test_doubleANumber() { assertEquals(6, classUnderTest.doubleANumber(3), "it should return 6"); } @Disabled public void test_not_executed() { fail("It should not executed"); } @Test @DisplayName("It should return false when input data isn't Save") public void test_returnBooleanFoo_false() { boolean shouldReturnFalse = classUnderTest.returnABoolean("NA"); assertFalse(shouldReturnFalse); } @Test @DisplayName("It should return true when input data is Save") public void test_returnBooleanFoo_true() { boolean shouldReturnTrue = classUnderTest.returnABoolean("Save"); assertTrue(shouldReturnTrue); testReporter.publishEntry(testInfo.getDisplayName()); } @Test public void test_voidFoo() throws IllegalAccessException { try { classUnderTest.voidFoo("OK"); } catch (Exception e) { fail("Should not throw exception"); } } @Test public void test_voidFoo_exception() throws IllegalAccessException { assertThrows(IllegalArgumentException.class, () -> { classUnderTest.voidFoo("NA"); }); } @Test public void test_timeout() { assertTimeout(Duration.ofMillis(1), ()-> classUnderTest.doubleANumber(1000)); } }
- Line 28 –
@BeforeEach
marks the method to be executed for each test. - Line 29 – can inject
TestInfo
andTestReporter
from Junit framework. - Line 35 –
@RepeatedTest
annotation is a new annotation in Junit 5 which executes the test repeatedly. - Line 40 –
@Disabled
annotation replaces the@Ignore
annotation in Junit 4. - Line 45 –
@Test
inorg.junit.jupiter.api
package marks a test. - Line 46 –
@DisplayName
is a new annotation which names the test with a more meaningful name. - Line 72 –
assertThrows
in JUnit 5 replaces the@Test
‘sexpected
attribute in Junit 4 . - Line 80 –
assertTimeout
in JUnit 5 replaces the@Test
‘stimeout
attribute in Junit 4 .
Output
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running jcg.zheng.demo.junit5.SomeClassTest doing something. [INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 s - in jcg.zheng.demo.junit5.SomeClassTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
In Eclipse IDE, you can see the test with the display name.
7. Summary
In this example, I demonstrated how to write a unit test in JUnit. Here are the major differences between JUnit 4 and JUnit 5:
JUnit 4 | JUnit 5 | |
Required JDK | 5 (+) | 8 (+) |
Package | org.junit | org.junit.jupiter |
Annotation | @Before | @BeforeEach |
@After | @AfterEach | |
@BeforeClass | @BeforeAll | |
@AfterClass | @AfterAll | |
@Ignore | @Disabled | |
– | @DisplayName | |
– | @RepeatedTest | |
@Category | @Tag | |
@RunWith | @ExtendWith |
8. Download the Source Code
You can download the full source code of this example here: Java Unit Testing with JUnit Example
I hate how-to’s that combine multiple technologies. They’re like lab experiments with multiple unknowns. Get rid of all the other tools, write the java code, and demonstrate JUnit to test it. Everything else is just noise.