Java Unit Testing with JUnit

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
Scroll to Top