Intro to Unit Testing in Java With JUnit5 Library
Check out this post to learn more about Java unit testing with JUnit 5.
Join the DZone community and get the full member experience.
Join For FreeHello, DZone! In this post, I would like to focus on unit testing in Java with the JUnit5 library. JUnit 5 introduced new features and approaches to testing — compared to the older JUnit 4 version — that are worth checking out. First, we will look at an overview of what unit testing is and why it is so important to test. Later, we will look at how to install JUnit 5 in your projects, what is a basic test structure, how to use the Assertions API, and finally, how to combine multiple tests in a test suite. Let's get started.
What Is Unit Testing?
Unit testing is a level of software testing where we test individual software's components in isolation. For example, we have UserService
. It may have various connected dependencies, like UserDAO
to connect to a datasource, or EmailProvider
to send confirmation emails. But for unit testing, we isolate theUserService
and may even mock connected dependencies (in regards to how to conduct mocking, we will dive deeper in the next section).
Unit testing offers us a number of benefits. Primarily, unit testing:
- Increases our confidence when we change code. If unit tests are well-written, and they are run every time our code is changed, we can notice any failures when we introduce new features
- Serves as documentation. Certainly, documenting your code includes several instruments, and unit testing is one of them — it describes the expected behavior of your code to other developers.
- Makes your code more reusable, as for good unit tests, code components should be modular.
These advantages are just a few of the many that are provided through unit testing. Now that we have defined unit testing and why we use it, we are ready to move on to JUnit5.
Install JUnit5
Build Tools Support
For native support of JUnit5, you should have a version of Gradle 4.6+ or Maven 2.22.0+.
For Maven, you need to add to your Pom.xml:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>{$version}</version>
<scope>test</scope>
</dependency>
For Gradle, add to build.gradle:
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '$version'
You could find the latest version of JUnit5 in the official repository. By the way, if you wish to use JUnit 5 with the Vert.x framework, there is Vert.x JUnit5 extension.
IDE Support
IntelliJ IDEA has supported JUnit5 natively since 2016.2, and Eclipse since 4.7.1a.
Anatomy of the Unit Test
Basic Structure
Consider this example: We have a program that performs a linear search of a number in an array of integers. Here is the main class that we place in the src/main/java/ folder:
class LinearSearcher(){
private int[] data;
LinearSearcher(int[] arr){
this.data = arr;
}
int getPositionOfNumber(int value){
int n = data.length;
for(int p = 0; i < n; i++)
{
if(data[p] == value)
return p;
}
return -1;
}
}
And then, we add this second piece of code to the src/test/java folder:
class LinearSearcherTest{
private static LinearSearcher searcher;
//before
@BeforeAll
static void setup(){
int[] array = {2, 3, 4, 10, 40};
searcher = new LinearSearcher(array);
}
//Actual test methods
@Test
void getPosition(){
int result = searcher.getPositionOfNumber(10);
Assertions.assertEquals(3,result);
}
@Test
void noSuchNumber(){
int result = searcher.getPositionOfNumber(55);
Assertions.assertEquals(-1, result);
}
//after
@AfterAll
static void finish(){
System.out.println("Tests are finished!");
}
}
Let's check what we did in this code. We introduced a new class, LinearSearcher
, that has one method — getPostionOfNumber
— that returns the position of the value in the array, or returns -1 if the value is not presented in the array.
In the second class, LinearSearcherTest
, we actually perform unit testing. We can expect two scenarios: When we have a number in the array (in our case 10), we expect to receive its position (3). If no such number is presented (for example 55), our searcher should return -1. Next, you can run this code and check the results.
Before Methods
You could note two methods annotated respectively with @BeforeAll
and @AfterAll
. What do they do? The first method corresponds to Before methods. There are two of them:
@BeforeAll
— the static method that will be executed once before all@Test
methods in the current class.@BeforeEach
— the method that will be executed before each@Test
method in the current class.
These methods are handy to set up the unit test environment (for example, to create instances).
After Methods
As there are Before methods, there are also After methods. There are a couple of them:
@AfterAll
— the static method executed once after all@Test
methods in the current class.@AfterEach
— the method that will be executed after each@Test
method in the current class.
Using Standard Assertions API
The Assertions API is a collection of utility methods that support asserting conditions in tests. There are numerous available methods; however, we will focus on the most important of them.
Assert Not Null
When we need to assert that the actual object is not null, we can use this method:
assertNotNull(Object obj);
If the object is not null, the method passes; if not, it fails.
Assert Equals
This group includes many methods, so I would not provide all overloaded versions but would focus a general signature:
assertEquals(expected_value, actual_value, optional_message);
These methods have two required arguments and one optional argument:
- expected_value = the result, we want to receive
- actual_value = the tested value
- optional_ message = the String message, that would be displayed to STDOUT if the method fails.
Values can be of primitive types: int
, double
, float
, long
, short
, boolean
, char
, byte
, as well Strings
and Objects
. To this group, we can add these test methods:
assertArrayEquals
— check that expected and actual arrays are equal. Arrays are of primitive typesAssertFalse
andAssertTrue
— check that supplied boolean condition is false or true respectivelyassertIterableEquals
— same asassertArrayEquals
, but for Iterables (e.g. List, Set, etc.)
As I mentioned, there are many overloaded methods in this section, so it's worth exploring the official documentation for concrete signatures.
Assert Throws
This is an innovation of JUnit5. Consider that you have a method that throws an exception:
Car findCarById(String id) throws FailedProviderException;
This method retrieves an individual Car
from an underlying database by its ID and throws FailedProviderException
when there is a problem with the database. In other words, we wrap in an interface possible data source exceptions (like SQLException
or respected for NoSQL databases) and achieve its independence from the implementation.
How do we test the exception thrown? Before, in JUnit4, we used annotations:
@Test(expected = FailedProviderException.class)
void exceptionThrownTest() throws Exception{
Car result = repository.findCarById("non-existing-id");
}
Also, by the way, the same idea is used in TestNG. In JUnit5, it was introduced as the assertThrows
method. Take a look at how we would deal with the same situation:
@Test
void exceptionThrownTest(){
Assertions.assertThrows(FailedProviderException.class, ()->{
Car result = repository.findCarById("non-existing-id");
});
}
This method signature has two components:
- Expected exception to be thrown
- Lambda expression of Executable that contains a code snippet, which potentially throws the exception.
Again, as with the mentioned previously methods of assertEquals
's group, we can provide an optional message of String as the third argument.
Assert Timeout
When we need to assert that the test is finished in a defined timeout, we can use this method:
assertTimeout(Duration timeout, Executable executable)
The idea is the same as with the assertThrows
method, but there, we specifytimeout
. The second argument is the same Executable lambda expression. A third optional component is a String message. Let's consider an example:
@Test
void in3secondsTest(){
Assertions.assertTimeout(Duration.ofSeconds(3), ()->{
//some code
});
}
Please note that this method uses Duration API to specify the timeframe. It has several handy methods, like ofSeconds()
, ofMills()
, etc. If you are not familiar with it, don't be shy to check out this tutorial.
Fail
Finally, what if we need to fail the test? Just use the Assertions.fail()
method. Again, there are several of them:
fail (String message)
= Fails a test with the given failure message.fail (String message, Throwable cause)
= Fails a test with the given failure message as well as the underlying cause.fail (Throwable cause)
= Fails a test with the given underlying cause.
Creating Test Suites
If you have several unit tests and you want to execute them in one load, you can create a test suite.
This approach allows you to run tests spread into multiple test classes and different packages.
Suppose that we have tests TestA, TestB, and TestC that are divided into three packages: net.mednikov.teststutorial.groupA, net.mednikov.teststutorial.groupA, net.mednikov.teststutorial.groupC respectively. We can write the test suite to combine them:
@RunWith(JUnitPlatform.class)
@SelectPackages({net.mednikov.teststutorial.groupA, net.mednikov.teststutorial.groupB, net.mednikov.teststutorial.groupC})
public class TestSuite(){}
Now, you can run this method as one test suite.
References
- Sergio Martin. Take Unit Testing to the Next Level With JUnit 5 (2018). DZone, read here
- Petri Kainulainen. Writing Assertions with JUnit5 Assertion API (2018), read here
- J Steven Perry. The JUnit5 Jupiter API (2017) IBM Developer, read here
Conclusion
In this post, we learned what is a unit test and why we test; how to install JUnit 5 in your project; what is a basic test structure; how to use the Assertions API; and how to combine multiple tests from different packages in a test suite. But, of course, JUnit 5 is a huge topic, and this post is just the tip of the iceberg. Some frameworks, like Vert.x, offer special JUnit 5 extensions, like vertx-junit5.
Good luck with JUnit5!
Published at DZone with permission of Yuri Mednikov. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments