
Unit tests verify that individual pieces of your code work correctly. They catch bugs early, make refactoring safer, and serve as documentation for how your code should behave. JUnit is Java’s standard testing framework, used by nearly every professional Java project.
This guide covers JUnit 5, the current version, with practical examples you can apply immediately.
Setting Up JUnit 5
Maven Dependency
Add JUnit 5 to your pom.xml:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
Gradle Dependency
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
Project Structure
Tests live in src/test/java, mirroring the main source structure:
src/
main/java/
com/example/
Calculator.java
test/java/
com/example/
CalculatorTest.java
Your First Test
Let’s test a simple Calculator class:
// src/main/java/com/example/Calculator.java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Cannot divide by zero");
}
return a / b;
}
}
// src/test/java/com/example/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calculator = new Calculator();
@Test
void addTwoPositiveNumbers() {
int result = calculator.add(2, 3);
assertEquals(5, result);
}
@Test
void subtractNumbers() {
assertEquals(7, calculator.subtract(10, 3));
}
@Test
void multiplyNumbers() {
assertEquals(20, calculator.multiply(4, 5));
}
@Test
void divideNumbers() {
assertEquals(4, calculator.divide(20, 5));
}
@Test
void divideByZeroThrowsException() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.divide(10, 0);
});
}
}
Run tests with Maven: mvn test
Run tests with Gradle: gradle test
JUnit Annotations
Core Annotations
import org.junit.jupiter.api.*;
class AnnotationExamplesTest {
@BeforeAll
static void setupOnce() {
// Runs once before all tests in the class
System.out.println("Starting test suite");
}
@AfterAll
static void teardownOnce() {
// Runs once after all tests complete
System.out.println("Test suite complete");
}
@BeforeEach
void setup() {
// Runs before each test method
System.out.println("Setting up test");
}
@AfterEach
void teardown() {
// Runs after each test method
System.out.println("Cleaning up after test");
}
@Test
void regularTest() {
// A normal test case
}
@Test
@DisplayName("User-friendly test name shown in reports")
void testWithDisplayName() {
// Test with custom name
}
@Test
@Disabled("Temporarily disabled - bug #123")
void skippedTest() {
// This test won't run
}
}
Execution Order
@BeforeAll (once)
@BeforeEach
@Test (first test)
@AfterEach
@BeforeEach
@Test (second test)
@AfterEach
@AfterAll (once)
Assertions
JUnit provides many assertion methods. Import them statically for cleaner code:
import static org.junit.jupiter.api.Assertions.*;
Basic Assertions
@Test
void basicAssertions() {
// Equality
assertEquals(4, 2 + 2);
assertEquals("hello", "hello");
assertNotEquals(5, 2 + 2);
// Boolean
assertTrue(5 > 3);
assertFalse(5 < 3);
// Null checks
assertNull(null);
assertNotNull("value");
// Same reference
String s1 = "test";
String s2 = s1;
assertSame(s1, s2);
// Floating point (with delta for precision)
assertEquals(0.3, 0.1 + 0.2, 0.0001);
}
Assertion Messages
@Test
void assertionsWithMessages() {
int expected = 100;
int actual = calculateValue();
// Message shown on failure
assertEquals(expected, actual, "Value calculation failed");
// Lazy message (computed only on failure)
assertEquals(expected, actual,
() -> "Expected " + expected + " but got " + actual);
}
Collection Assertions
@Test
void collectionAssertions() {
List<String> expected = List.of("a", "b", "c");
List<String> actual = getList();
// Lists equal (order matters)
assertEquals(expected, actual);
// Arrays equal
assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});
// Iterable contains exactly (order matters)
assertIterableEquals(expected, actual);
}
Exception Assertions
@Test
void exceptionAssertions() {
// Assert exception is thrown
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("test");
});
// Capture and inspect exception
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("Invalid input");
});
assertEquals("Invalid input", exception.getMessage());
// Assert no exception thrown
assertDoesNotThrow(() -> {
int result = 10 / 2;
});
}
Grouped Assertions
@Test
void groupedAssertions() {
User user = new User("Alice", 30, "alice@example.com");
// All assertions run even if some fail
assertAll("user properties",
() -> assertEquals("Alice", user.getName()),
() -> assertEquals(30, user.getAge()),
() -> assertEquals("alice@example.com", user.getEmail())
);
}
Parameterized Tests
Run the same test with different inputs:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class ParameterizedTestExamples {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testPositiveNumbers(int number) {
assertTrue(number > 0);
}
@ParameterizedTest
@ValueSource(strings = {"hello", "world", "junit"})
void testStringNotEmpty(String value) {
assertFalse(value.isEmpty());
}
@ParameterizedTest
@NullAndEmptySource
void testNullAndEmpty(String value) {
assertTrue(value == null || value.isEmpty());
}
@ParameterizedTest
@EnumSource(Month.class)
void testAllMonths(Month month) {
assertNotNull(month);
}
@ParameterizedTest
@CsvSource({
"1, 2, 3",
"5, 5, 10",
"10, -5, 5"
})
void testAddition(int a, int b, int expected) {
Calculator calc = new Calculator();
assertEquals(expected, calc.add(a, b));
}
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
void testFromCsvFile(String input, int expected) {
// Reads data from src/test/resources/test-data.csv
}
@ParameterizedTest
@MethodSource("provideStringsForLength")
void testStringLength(String input, int expectedLength) {
assertEquals(expectedLength, input.length());
}
static Stream<Arguments> provideStringsForLength() {
return Stream.of(
Arguments.of("hello", 5),
Arguments.of("world", 5),
Arguments.of("", 0)
);
}
}
Test Lifecycle and Setup
class UserServiceTest {
private UserService userService;
private UserRepository mockRepository;
@BeforeEach
void setup() {
mockRepository = new MockUserRepository();
userService = new UserService(mockRepository);
}
@Test
void createUserSuccessfully() {
User user = new User("Alice", "alice@example.com");
User created = userService.createUser(user);
assertNotNull(created.getId());
assertEquals("Alice", created.getName());
}
@Test
void findUserById() {
// Test uses fresh userService instance
User found = userService.findById(1L);
assertNotNull(found);
}
}
Nested Tests
Organize related tests into groups:
@DisplayName("Calculator Tests")
class CalculatorNestedTest {
Calculator calculator = new Calculator();
@Nested
@DisplayName("Addition")
class AdditionTests {
@Test
@DisplayName("adds two positive numbers")
void addPositive() {
assertEquals(5, calculator.add(2, 3));
}
@Test
@DisplayName("adds negative numbers")
void addNegative() {
assertEquals(-5, calculator.add(-2, -3));
}
@Test
@DisplayName("adds zero")
void addZero() {
assertEquals(5, calculator.add(5, 0));
}
}
@Nested
@DisplayName("Division")
class DivisionTests {
@Test
@DisplayName("divides evenly")
void divideEvenly() {
assertEquals(4, calculator.divide(20, 5));
}
@Test
@DisplayName("throws on divide by zero")
void divideByZero() {
assertThrows(IllegalArgumentException.class,
() -> calculator.divide(10, 0));
}
}
}
Timeouts
@Test
@Timeout(5) // Fails if takes more than 5 seconds
void testWithTimeout() {
// Slow operation
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void testWithMillisecondTimeout() {
// Must complete in 500ms
}
@Test
void testAssertTimeout() {
// Assert operation completes within time
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(1000);
return "result";
});
}
Mocking with Mockito
For testing classes with dependencies, use Mockito to create mock objects:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private EmailService emailService;
private OrderService orderService;
@BeforeEach
void setup() {
orderService = new OrderService(orderRepository, emailService);
}
@Test
void createOrderSuccessfully() {
// Arrange - set up mock behavior
Order order = new Order(1L, "Product", 100.00);
when(orderRepository.save(any(Order.class))).thenReturn(order);
// Act
Order created = orderService.createOrder("Product", 100.00);
// Assert
assertEquals(1L, created.getId());
assertEquals("Product", created.getName());
// Verify interactions
verify(orderRepository).save(any(Order.class));
verify(emailService).sendOrderConfirmation(any(Order.class));
}
@Test
void findOrderNotFound() {
when(orderRepository.findById(999L)).thenReturn(Optional.empty());
assertThrows(OrderNotFoundException.class,
() -> orderService.findById(999L));
}
}
Test Best Practices
Name Tests Clearly
// Bad
@Test
void test1() { }
// Good
@Test
void createUserWithValidEmailSucceeds() { }
@Test
void createUserWithInvalidEmailThrowsException() { }
Follow Arrange-Act-Assert
@Test
void withdrawReducesBalance() {
// Arrange
BankAccount account = new BankAccount(100.00);
// Act
account.withdraw(30.00);
// Assert
assertEquals(70.00, account.getBalance(), 0.001);
}
Test One Thing Per Test
// Bad - tests multiple behaviors
@Test
void userOperations() {
User user = service.createUser("Alice");
assertNotNull(user);
user.setEmail("alice@example.com");
service.updateUser(user);
assertEquals("alice@example.com", service.findById(user.getId()).getEmail());
service.deleteUser(user.getId());
assertNull(service.findById(user.getId()));
}
// Good - separate tests
@Test
void createUserSuccessfully() {
User user = service.createUser("Alice");
assertNotNull(user);
}
@Test
void updateUserEmail() {
User user = createTestUser();
user.setEmail("alice@example.com");
service.updateUser(user);
assertEquals("alice@example.com", service.findById(user.getId()).getEmail());
}
@Test
void deleteUserRemovesFromDatabase() {
User user = createTestUser();
service.deleteUser(user.getId());
assertNull(service.findById(user.getId()));
}
Use Meaningful Assertions
// Bad - unclear failure message
@Test
void testUser() {
assertTrue(user.getName().equals("Alice"));
}
// Good - clear failure message
@Test
void userHasCorrectName() {
assertEquals("Alice", user.getName());
}
Running Tests
Command Line
# Maven
mvn test # Run all tests
mvn test -Dtest=CalculatorTest # Run specific test class
mvn test -Dtest=CalculatorTest#addTwoPositiveNumbers # Run specific method
# Gradle
gradle test
gradle test --tests CalculatorTest
gradle test --tests "CalculatorTest.addTwoPositiveNumbers"
IDE Integration
IntelliJ IDEA, Eclipse, and VS Code all provide built-in JUnit support. Right-click a test class or method and select “Run” to execute tests with visual feedback.
Summary
Unit testing with JUnit is essential for professional Java development. Start by testing public methods with clear inputs and outputs. Use mocks for external dependencies. Write tests that are fast, isolated, and focused on one behavior.
Good tests give you confidence to refactor and deploy. They catch regressions before users do. The time invested in testing pays back many times over.
Prerequisites: Java Methods | Java Exception Handling | Introduction to Maven
Related: Java Design Patterns | Introduction to Spring Boot
Sources
- JUnit Team. “JUnit 5 User Guide.” junit.org/junit5/docs/current/user-guide
- Mockito. “Mockito Documentation.” site.mockito.org


