JUnit Essentials
JUnit
JUnit is an open-source and the most popular unit testing framework used in Java. It provides annotations and utilities to write the unit test cases in a simpler way. Most IDEs support JUnit and provide a platform to execute and visualize JUnit test results. The code snippet written in this article follows JUnit 5.
1. @DisplayName
Test method names tend to be always long and messy. The method name highlights what the test case is doing. Also, the method name appears as the test case name in your IDE or Unit test report such as SureFire.
Example: A test case to verify if the exception IllegalArgumentException is thrown while adding two numbers if any of the arguments is negative.
// The class to test
// MathOp.java
class MathOp {
public int add(int x, int y) {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Arguments cannot be negative");
}
return x + y;
}
}
// The test case to test the above class
// MathOpTest.java
class MathOpTest {
@Test
void test_IllegalArgumentException_when_args_are_negative() {
int x = 10, y = -5;
Assertions.assertThrows(IllegalArgumentException.class, () -> MathOp.add(x, y));
}
// Otherwise, if you are a strict follower of the camel convention
@Test
void testIllegalArgumentExceptionThrownWhenArgAreNegative() {
int x = 10, y = -5;
Assertions.assertThrows(IllegalArgumentException.class, () -> MathOp.add(x, y));
}
}
Using the annotation @DisplayName we can provide a meaningful name to the test case and avoid long names as shown in the below example.
@Test
@DisplayName("Expect IllegalArgumentException when negative numbers passed")
void negativeArgs() {
int x = 10, y = -5;
Assertions.assertThrows(IllegalArgumentException.class, () -> MathOp.add(x, y));
}
2. @RepeatedTest
This @RepeatedTest annotation repeats the execution of a test method for a specified number of times.
Example:
@RepeatedTest(value = 3, name = "Sum of 3 and 2 should always be 5")
void add() {
Assertions.assertEquals(5, MathOp.add(3, 2), "No matter how many times you add 3 and 2, you must always get the result 5");
}
3. @Disabled
During the development, we might want to skip some test cases because it is under modification or it is uncertain. In such cases, we can use this annotation to exclude/skip such test cases. This annotation can be applied over a test class or individual test methods as well.
class MathOpsTest {
@Disabled("Disable until issue #467 got fixed")
@Test
void factorial() {
Assertions.assertEquals(120, MathOps.factorial(5));
}
}
// You can also use this annotation to exclude an entire test class
@Disabled
class MathOpsTest {
// test cases...
}
4. @Nested
Test classes easily get crowded with lots of methods which cover several scenarios of an actual method of a real class. The @Nested annotation helps us to group the test cases. Grouping the test cases provides more readability and maintainability.
Example: The below test class shows how it looks without grouping.
// Test cases for add, divide, prime
class MathUtils {
@Test
void add() {
Assertions.assertEquals(4, MathUtils.add(3, 1));
}
@Test
void addNegativeNumbers() {
Assertions.assertEquals(-5, MathUtils.add(-2, -3));
}
@Test
void divide() {
Assertions.assertEquals(6.66665, MathUtils.divide(20, 3));
}
@Test
void divideByZero() {
Assertions.assertThrows(ArithmeticException.class, MathUtils.divide(2, 0));
}
@Test
void divideBothNegative() {
Assertions.assertEquals(-4, MathUtils.divide(-8, 2));
}
@Test
void divideBothPositive() {
Assertions.assertEquals(4, MathUtils.divide(-8, -2));
}
@Test
void isPrime() {
Assertions.assertTrue(MathUtils.isPrime(7));
}
@Test
void isPrimeNegativeNum() {
Assertions.assertThrows(IllegalArgumentException.class, MathUtils.isPrime(-7));
}
@Test
void isPrimeOutOfRange() {
Assertions.assertThrows(OutOfRangeException.class, MathUtils.isPrime(10039023));
}
}
In the above test cases, you can see there are two test cases for adding two numbers, four test cases for dividing, and three test cases for checking the prime numbers. All these test cases are written in a single class file. In the real application, there can be more than 10 scenarios to test a single method. Hence the class looks crowded. We can group this using the @nested annotation.
class MathUtils {
@Nested
class AddTest {
@Test
void add() {
Assertions.assertEquals(4, MathUtils.add(3, 1));
}
@Test
void addNegativeNumbers() {
Assertions.assertEquals(-5, MathUtils.add(-2, -3));
}
}
@Nested
class DivideTest {
@Test
void divide() {
Assertions.assertEquals(6.66665, MathUtils.divide(20, 3));
}
@Test
void divideByZero() {
Assertions.assertThrows(ArithmeticException.class, MathUtils.divide(2, 0));
}
@Test
void divideBothNegative() {
Assertions.assertEquals(-4, MathUtils.divide(-8, 2));
}
@Test
void divideBothPositive() {
Assertions.assertEquals(4, MathUtils.divide(-8, -2));
}
}
@Nested
class PrimeTest {
@Test
void isPrime() {
Assertions.assertTrue(MathUtils.isPrime(7));
}
@Test
void isPrimeNegativeNum() {
Assertions.assertThrows(IllegalArgumentException.class, MathUtils.isPrime(-7));
}
@Test
void isPrimeOutOfRange() {
Assertions.assertThrows(OutOfRangeException.class, MathUtils.isPrime(10039023));
}
}
}
5. @TestMethodOrder
JUnit runs test cases in an unpredictable order. Sometimes we want to order the execution of methods in a certain order. The annotation @TestMethodOrder helps us to achieve the execution method order.
Example 1: Order using the @Order annotation
@TestMethodOrder(OrderAnnotation.class)
class StringUtilsTest {
@Order(4)
@Test
void isEmpty() {
Assertions.assertTrue(StringUtils.isEmpty(""));
Assertions.assertTrue(StringUtils.isEmpty(null));
}
@Order(2)
@Test
void titleCase() {
Assertions.assertEquals("Title", StringUtils.titleCase("titLE"));
}
@Order(1)
@Test
void trim() {
Assertions.assertEquals("computer", StringUtils.titleCase(" computer "));
}
@Order(3)
@Test
void reverse() {
Assertions.assertEquals("esrever", StringUtils.titleCase("reverse"));
}
}
Example 2: Sort by name of the method
@TestMethodOrder(MethodOrderer.MethodName.class)
class StringUtilsTest {
@Test
void methodCisEmpty() {
Assertions.assertTrue(StringUtils.isEmpty(""));
Assertions.assertTrue(StringUtils.isEmpty(null));
}
@Test
void methodBtitleCase() {
Assertions.assertEquals("Title", StringUtils.titleCase("titLE"));
}
@Test
void methodAtrim() {
Assertions.assertEquals("computer", StringUtils.titleCase(" computer "));
}
@Test
void methodCreverse() {
Assertions.assertEquals("esrever", StringUtils.titleCase("reverse"));
}
}
Also, it supports random and custom orders.
6. @BeforeAll, @BeforeEach, @AfterAll, @AfterEach
These methods are used to execute some logic such as initializing an embedded test database or file, initializing a static list for testing data, and tearing down purposes.
Here is a simple example:
class StringUtilsTest {
@BeforeAll
public void setup() {
System.out.println("I run only once before you start executing this test class");
}
@BeforeEach
public void reInitialize() {
System.out.println("I run everytime before executing each test case");
}
@AfterAll
public void tearDown() {
System.out.println("I only once after you finish executing all the test cases");
}
@AfterEach
public void cleanUpEveryTime() {
System.out.println("I run everytime after executing each test cases");
}
@Test
void testStartsWith() {
Assertions.assertTrue(StringUtils.startsWith("alphabet", "alpha"));
}
@Test
void testEndsWith() {
Assertions.assertTrue(StringUtils.endsWith("alphabet", "bet"));
}
}
Assertions and Assumptions
- ‘Assertion’ fails the test if the condition failed.
- ‘Assumption’ skips the test if the condition failed.
A basic example of ‘assumption’ is that, we execute the test case createEmployee if it’s running in the dev environment, otherwise, we want to skip it.
class EmployeeRepositoryTest {
@Test
void createEmployee() {
Assumptions.assumeTrue("dev".equals(System.getenv()));
Employee emp = new Employee(123, "test");
Employee saved = employeeRepo.save(emp);
Assertions.assertNotNull(saved);
}
}
Parameterized Test
The Parameterized test is a cool new feature introduced in JUnit 5. It reduces the lines of code, saves time, eliminates duplicates, and makes the test cases more simpler and readable. There is a separate article that describes the Parameterized test in JUnit.