Unit Testing in Java with JUnit 5: Annotations, Assertions, and Mockito

In enterprise Java software engineering, writing clean, maintainable, and bug-free code is only half the battle. Ensuring that business logic behaves predictably under edge cases requires a robust automated testing strategy. **JUnit 5** is the modern standard testing framework for Java, introducing a modular architecture (`JUnit Platform`, `JUnit Jupiter`, and `JUnit Vintage`), powerful annotations, parameterized testing, and nested test structures.

This comprehensive guide explores the core components of JUnit 5 combined with **Mockito** for dependency isolation, equipping Java developers to build thorough, production-grade test suites.

1. JUnit 5 Core Annotations and Lifecycle Hooks

JUnit 5 relies on clean, intuitive annotations to manage test execution lifecycles and organize test methods.

C
import org.junit.jupiter.api.*;

public class CalculatorTest {

    @BeforeAll
    static void initAll() {
        System.out.println("Initializing test suite resources...");
    }

    @BeforeEach
    void setUp() {
        // Executed before each individual test method
    }

    @Test
    @DisplayName("Test addition of two positive integers")
    void testAddition() {
        int result = 2 + 3;
        Assertions.assertEquals(5, result, "Calculated sum should be 5");
    }

    @AfterEach
    void tearDown() {
        // Cleanup after each test
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("Test suite execution completed.");
    }
}

2. Advanced Assertions and Exception Testing

JUnit 5 provides static assertion methods (`Assertions.*`) with lambda support for lazy evaluation, alongside robust tools for testing expected exceptions.

C
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ValidationTest {

    @Test
    void testExceptionThrowing() {
        IllegalArgumentException exception = Assertions.assertThrows(
            IllegalArgumentException.class,
            () -> {
                throw new IllegalArgumentException("Invalid age provided");
            }
        );
        
        Assertions.assertEquals("Invalid age provided", exception.getMessage());
    }
}

3. Parameterized Tests in JUnit 5

Instead of duplicating test code for different input variations, `@ParameterizedTest` allows you to execute a single test method across multiple arguments using sources like `@ValueSource` or `@CsvSource`.

C
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class StringUtilsTest {

    @ParameterizedTest
    @CsvSource({
        "radar, true",
        "hello, false",
        "level, true",
        "java, false"
    })
    void testIsPalindrome(String input, boolean expected) {
        boolean result = input.equals(new StringBuilder(input).reverse().toString());
        Assertions.assertEquals(expected, result);
    }
}

4. Mocking External Dependencies with Mockito

Enterprise services often depend on external databases, repositories, or HTTP clients. Mocking frameworks like **Mockito** isolate unit tests by creating mock implementations of dependencies.

C
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

// Mocking interface service
interface UserRepository {
    String findUserEmailById(int userId);
}

class UserService {
    private final UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public String getUserDomain(int userId) {
        String email = repository.findUserEmailById(userId);
        return email.substring(email.indexOf("@") + 1);
    }
}

public class UserServiceTest {

    @Test
    void testGetUserDomain() {
        // 1. Create mock repository
        UserRepository mockRepo = Mockito.mock(UserRepository.class);
        
        // 2. Stub behavior
        Mockito.when(mockRepo.findUserEmailById(101)).thenReturn("admin@codecrown.in");
        
        // 3. Inject mock into service
        UserService service = new UserService(mockRepo);
        
        // 4. Assert outcome
        String domain = service.getUserDomain(101);
        Assertions.assertEquals("codecrown.in", domain);
        
        // 5. Verify interaction
        Mockito.verify(mockRepo, Mockito.times(1)).findUserEmailById(101);
    }
}

Conclusion

Mastering unit testing in Java with JUnit 5 and Mockito—from organizing test lifecycles and writing parameterized checks to isolating business logic with mocks—enables software engineers to build secure, highly reliable enterprise applications.