Today we'll cover the basics of Unit testing using JUnit 5.
Let's figure out how to use this power to build high-quality, safe code.
What is JUnit 5 and why do we need it?
JUnit 5 is the modern version of the popular testing framework for Java. Its goal is to help developers write solid Unit tests. The advantage of JUnit 5 is its modularity and flexibility. Here are the main components of JUnit 5:
- JUnit Platform: responsible for running tests.
- JUnit Jupiter: provides new features and annotations for testing.
- JUnit Vintage: allows running older tests written for JUnit 3 and 4 (archaeologists, this one's for you).
Why JUnit 5? Because:
- It supports Java 8 and above, including lambdas and default methods.
- More options to configure the test lifecycle.
- The lovely
@DisplayNameannotation to make your tests look nice and clear.
Main JUnit 5 annotations
Annotations are like pointers for JUnit that tell it what and how to test.
@Test
The main annotation. If you want code to run as a test, just mark the method with this annotation.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MathTest {
@Test
void additionTest() {
int result = 2 + 3;
assertEquals(5, result, "2 + 3 should equal 5");
}
}
@BeforeEach and @AfterEach
Used to run actions before and after each test. For example, setting up the test environment or cleaning resources.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
class LifecycleTest {
@BeforeEach
void setUp() {
System.out.println("Setup before test");
}
@AfterEach
void tearDown() {
System.out.println("Cleanup after test");
}
@Test
void simpleTest() {
System.out.println("Running the test");
}
}
The output will be:
Setup before test
Running the test
Cleanup after test
@BeforeAll and @AfterAll
Used to perform actions before or after all tests in the class. These methods must be static.
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
class GlobalSetupTest {
@BeforeAll
static void init() {
System.out.println("Setup before ALL tests");
}
@AfterAll
static void cleanup() {
System.out.println("Cleanup after ALL tests");
}
@Test
void testOne() {
System.out.println("First test");
}
@Test
void testTwo() {
System.out.println("Second test");
}
}
Output:
Setup before ALL tests
First test
Second test
Cleanup after ALL tests
@DisplayName
Add some greenery to the digital desert of your tests! Make them prettier with @DisplayName.
@Test
@DisplayName("Addition check: 2 + 2 = 4")
void additionTest() {
assertEquals(4, 2 + 2);
}
Basic Assertions
Assertions are like checkpoints. They verify that your code behaves as expected.
| Assertion | Description | Example |
|---|---|---|
assertEquals(expected, actual) |
Checks whether the expected and actual values are equal | assertEquals(4, 2 + 2) |
assertNotEquals(unexpected, actual) |
Checks that values are NOT equal | assertNotEquals(5, 2 + 2) |
assertTrue(condition) |
Asserts that the condition is true | assertTrue(2 + 2 == 4) |
assertFalse(condition) |
Asserts that the condition is false | assertFalse(2 + 2 == 5) |
assertNull(object) |
Checks that the object is null | assertNull(nullVariable) |
assertNotNull(object) |
Checks that the object is NOT null | assertNotNull(nonNullVariable) |
assertThrows(exception, lambda) |
Checks that the code throws the specified exception | assertThrows(NumberFormatException.class, () -> Integer.parseInt("NaN")) |
Example:
@Test
void testAssertions() {
assertEquals(4, 2 + 2, "Adding two numbers should be correct");
assertTrue(3 > 2, "3 > 2 is true");
assertThrows(ArithmeticException.class, () -> {
int result = 10 / 0;
}, "Expected ArithmeticException when dividing by 0");
}
Test lifecycle in JUnit 5
Tests in JUnit go through a specific lifecycle:
- An instance of the test class is created.
- The method annotated with @BeforeEach is called.
- The test runs.
- The method annotated with @AfterEach is called.
- This repeats for every test.
Methods annotated with @BeforeAll and @AfterAll run once per class.
Practice: Writing Unit tests for simple methods
Suppose we have a class Calculator:
class Calculator {
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero is not allowed");
}
return a / b;
}
}
And tests for it:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
Calculator calculator = new Calculator();
@Test
void testAddition() {
assertEquals(5, calculator.add(2, 3), "2 + 3 should equal 5");
}
@Test
void testSubtraction() {
assertEquals(1, calculator.subtract(3, 2), "3 - 2 should equal 1");
}
@Test
void testMultiplication() {
assertEquals(6, calculator.multiply(2, 3), "2 * 3 should equal 6");
}
@Test
void testDivision() {
assertEquals(2, calculator.divide(6, 3), "6 / 3 should equal 2");
}
@Test
void testDivisionByZero() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> calculator.divide(5, 0));
assertEquals("Division by zero is not allowed", exception.getMessage());
}
}
These tests check basic arithmetic operations and handle the exception when dividing by zero.
What usually confuses people the most?
- Why aren't my tests running? Maybe your method in the test class isn't annotated with
@Test. - Why does a test "fail"? Read the error message carefully. Most likely the expectations in
assertEqualsdon't match the actual result. - How to test private methods? Often private methods are tested indirectly through public ones.
JUnit 5 is a powerful tool, and the more you practice with it, the easier it gets to write solid tests. Go forth — smash your bugs in true superhero fashion!
GO TO FULL VERSION