Web Analytics

Unit Testing with JUnit

Advanced ~20 min read

Unit testing is a critical part of professional software development. JUnit is the most popular testing framework for Java, enabling you to write automated, repeatable tests that verify your code works correctly.

Why Unit Testing?

  • Catch bugs early: Find issues before they reach production
  • Refactor with confidence: Change code knowing tests will catch regressions
  • Documentation: Tests show how code is meant to be used
  • Better design: Testable code is usually better designed

JUnit 5 Setup (Maven)

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Basic Test Structure

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();  // Fresh instance for each test
    }

    @Test
    @DisplayName("Adding two positive numbers")
    void testAddPositiveNumbers() {
        assertEquals(5, calculator.add(2, 3));
    }

    @Test
    void testAddNegativeNumbers() {
        assertEquals(-5, calculator.add(-2, -3));
    }

    @Test
    void testDivideByZero() {
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
    }
}

Common Assertions

Assertion Description
assertEquals(expected, actual) Values are equal
assertNotEquals(a, b) Values are not equal
assertTrue(condition) Condition is true
assertFalse(condition) Condition is false
assertNull(object) Object is null
assertNotNull(object) Object is not null
assertThrows(Exception.class, () -> ...) Code throws exception
assertArrayEquals(arr1, arr2) Arrays are equal

Lifecycle Annotations

class LifecycleTest {

    @BeforeAll
    static void initAll() {
        // Runs once before all tests (e.g., database connection)
    }

    @BeforeEach
    void init() {
        // Runs before each test (e.g., reset state)
    }

    @Test
    void testSomething() {
        // Your test
    }

    @AfterEach
    void tearDown() {
        // Runs after each test (e.g., cleanup)
    }

    @AfterAll
    static void tearDownAll() {
        // Runs once after all tests (e.g., close connection)
    }
}

Parameterized Tests

Run the same test with different inputs:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedTests {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void testIsPositive(int number) {
        assertTrue(number > 0);
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, 20, 30"
    })
    void testAdd(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }

    @ParameterizedTest
    @MethodSource("provideStringsForIsBlank")
    void testIsBlank(String input, boolean expected) {
        assertEquals(expected, StringUtils.isBlank(input));
    }

    static Stream<Arguments> provideStringsForIsBlank() {
        return Stream.of(
            Arguments.of(null, true),
            Arguments.of("", true),
            Arguments.of("  ", true),
            Arguments.of("hello", false)
        );
    }
}

Testing Best Practices

AAA Pattern:
  • Arrange: Set up test data and conditions
  • Act: Execute the code being tested
  • Assert: Verify the results
@Test
void testUserRegistration() {
    // Arrange
    UserService service = new UserService();
    User user = new User("[email protected]", "password123");

    // Act
    boolean result = service.register(user);

    // Assert
    assertTrue(result);
    assertNotNull(service.findByEmail("[email protected]"));
}

Test Naming Conventions

// Good test names describe behavior
@Test void shouldReturnTrueWhenUserIsAdmin() { }
@Test void shouldThrowExceptionWhenEmailIsInvalid() { }
@Test void givenValidUser_whenRegister_thenSuccess() { }

// Avoid vague names
@Test void test1() { }  // Bad
@Test void testUser() { }  // Bad

Mocking with Mockito

Mock dependencies to isolate the code under test:

import static org.mockito.Mockito.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void testFindUser() {
        // Arrange - define mock behavior
        User mockUser = new User("[email protected]");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));

        // Act
        User result = userService.findById(1L);

        // Assert
        assertEquals("[email protected]", result.getEmail());
        verify(userRepository, times(1)).findById(1L);
    }
}

Summary

  • Use JUnit 5 for modern Java testing
  • Follow AAA pattern: Arrange, Act, Assert
  • Use @BeforeEach for test setup
  • Use parameterized tests to reduce duplication
  • Use Mockito to mock dependencies
  • Write descriptive test names that explain behavior