Skip to main content

13 - Testing in Java: JUnit and Mockito

Testing in Java with JUnit and Mockito Automated tests are fundamental for ensuring code quality and maintainability. Learn how to write effective unit tests using JUnit 5 and Mockito to verify behavior, isolate dependencies, and build confidence in your Java applications.

What do you think about this article?

At a Glance

Testing in Java with JUnit and Mockito Automated tests are fundamental for ensuring code quality and maintainability. Learn how to write effective unit tests using JUnit 5 and Mockito to verify behavior, isolate dependencies, and build confidence in your Java applications.

Testing in Java with JUnit and Mockito

Automated tests are fundamental to ensure code quality and maintainability. JUnit is the standard framework for unit testing in Java, while Mockito allows creating mock objects.

What You'll Learn

  • JUnit 5: annotations and assertions
  • Parameterized tests
  • Mockito: mock, stub and verify
  • Test-Driven Development (TDD)
  • Testing best practices

JUnit 5 - Fundamentals

Basic Test with JUnit 5
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calc;

    @BeforeEach
    void setUp() {
        calc = new Calculator();
    }

    @Test
    @DisplayName("Addition of two positive numbers")
    void testAddition() {
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void testSubtraction() {
        assertEquals(2, calc.subtract(5, 3));
    }

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

    @Test
    @Disabled("Test temporarily disabled")
    void testToImplement() {
        fail("Not yet implemented");
    }
}

class Calculator {
    int add(int a, int b) { return a + b; }
    int subtract(int a, int b) { return a - b; }
    int divide(int a, int b) { return a / b; }
}

JUnit 5 Annotations

Annotation Description
@Test Marks a method as a test
@BeforeEach Executed before each test
@AfterEach Executed after each test
@BeforeAll Executed once before all tests
@DisplayName Human-readable test name
@Disabled Temporarily disables the test

Advanced Assertions

JUnit Assertions
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.*;

class AssertionsTest {

    @Test
    void basicAssertions() {
        // Equality
        assertEquals(4, 2 + 2);
        assertNotEquals(5, 2 + 2);

        // Booleans
        assertTrue(3 > 2);
        assertFalse(2 > 3);

        // Null
        Object obj = null;
        assertNull(obj);
        assertNotNull(new Object());

        // Same (same instance)
        String s1 = "hello";
        String s2 = s1;
        assertSame(s1, s2);
    }

    @Test
    void collectionAssertions() {
        List<String> list = Arrays.asList("A", "B", "C");

        assertEquals(3, list.size());
        assertTrue(list.contains("B"));

        // Arrays
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 3};
        assertArrayEquals(expected, actual);
    }

    @Test
    void exceptionAssertions() {
        Exception ex = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("Error message");
        });
        assertEquals("Error message", ex.getMessage());
    }

    @Test
    void timeoutAssertions() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            Thread.sleep(500);  // Must complete within 2 seconds
        });
    }

    @Test
    void groupedAssertions() {
        Student s = new Student("John", 25);

        assertAll("Verify student",
            () -> assertEquals("John", s.getName()),
            () -> assertEquals(25, s.getAge()),
            () -> assertTrue(s.getAge() > 0)
        );
    }
}

class Student {
    private String name;
    private int age;
    Student(String name, int age) { this.name = name; this.age = age; }
    String getName() { return name; }
    int getAge() { return age; }
}

Parameterized Tests

Tests with Parameters
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTestsDemo {

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

    @ParameterizedTest
    @ValueSource(strings = {"hello", "world", "java"})
    void testNonEmptyStrings(String str) {
        assertFalse(str.isEmpty());
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, 20, 30"
    })
    void testSum(int a, int b, int result) {
        assertEquals(result, a + b);
    }

    @ParameterizedTest
    @MethodSource("provideNumbers")
    void testWithMethodSource(int number, boolean expected) {
        assertEquals(expected, number % 2 == 0);
    }

    static Stream<Arguments> provideNumbers() {
        return Stream.of(
            Arguments.of(2, true),
            Arguments.of(3, false),
            Arguments.of(4, true)
        );
    }

    @ParameterizedTest
    @EnumSource(Month.class)
    void testMonths(Month month) {
        assertNotNull(month);
    }

    enum Month { JANUARY, FEBRUARY, MARCH }
}

Mockito - Mock Objects

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

// Interface to mock
interface UserRepository {
    User findById(Long id);
    void save(User user);
    List<User> findAll();
}

class User {
    private Long id;
    private String name;

    User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    Long getId() { return id; }
    String getName() { return name; }
}

class UserService {
    private final UserRepository repository;

    UserService(UserRepository repository) {
        this.repository = repository;
    }

    User getUser(Long id) {
        return repository.findById(id);
    }

    void createUser(User user) {
        repository.save(user);
    }
}

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository mockRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void testGetUser() {
        // Arrange: configure the mock
        User expectedUser = new User(1L, "John");
        when(mockRepository.findById(1L)).thenReturn(expectedUser);

        // Act: execute the method
        User result = userService.getUser(1L);

        // Assert: verify
        assertEquals("John", result.getName());
        verify(mockRepository).findById(1L);
    }

    @Test
    void testCreateUser() {
        User newUser = new User(2L, "Jane");

        userService.createUser(newUser);

        // Verify save was called
        verify(mockRepository, times(1)).save(newUser);
    }

    @Test
    void testUserNotFound() {
        when(mockRepository.findById(99L)).thenReturn(null);

        User result = userService.getUser(99L);

        assertNull(result);
    }
}

Advanced Mockito

Stub, Verify and ArgumentCaptor
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

class AdvancedMockitoTest {

    @Test
    void testAdvancedStub() {
        List<String> mockList = mock(List.class);

        // Stub with any matcher
        when(mockList.get(anyInt())).thenReturn("element");
        assertEquals("element", mockList.get(5));

        // Stub with different behavior
        when(mockList.size())
            .thenReturn(1)
            .thenReturn(2)
            .thenReturn(3);

        assertEquals(1, mockList.size());  // First call
        assertEquals(2, mockList.size());  // Second
        assertEquals(3, mockList.size());  // Third and onwards

        // Stub that throws exception
        when(mockList.get(100)).thenThrow(new IndexOutOfBoundsException());
    }

    @Test
    void testAdvancedVerify() {
        List<String> mockList = mock(List.class);

        mockList.add("one");
        mockList.add("two");
        mockList.add("one");

        // Verify call count
        verify(mockList, times(2)).add("one");
        verify(mockList, times(1)).add("two");
        verify(mockList, never()).add("three");
        verify(mockList, atLeast(1)).add(anyString());
        verify(mockList, atMost(3)).add(anyString());
    }

    @Test
    void testArgumentCaptor() {
        UserRepository mockRepo = mock(UserRepository.class);
        UserService service = new UserService(mockRepo);

        // Capture argument passed to save
        ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);

        service.createUser(new User(1L, "Test"));

        verify(mockRepo).save(captor.capture());
        User capturedUser = captor.getValue();

        assertEquals("Test", capturedUser.getName());
    }

    @Test
    void testPartialSpy() {
        // Spy: real object with some parts mocked
        List<String> realList = new ArrayList<>();
        List<String> spyList = spy(realList);

        spyList.add("element");  // Calls real method
        assertEquals(1, spyList.size());

        // Override a specific method
        doReturn(100).when(spyList).size();
        assertEquals(100, spyList.size());
    }
}

Test-Driven Development (TDD)

TDD Cycle
// TDD follows the cycle: RED -> GREEN -> REFACTOR

// STEP 1 - RED: Write the test BEFORE the code
class ShoppingCartTest {

    @Test
    void emptyCartHasZeroTotal() {
        ShoppingCart cart = new ShoppingCart();
        assertEquals(0.0, cart.getTotal(), 0.001);
    }

    @Test
    void addProductCalculatesTotal() {
        ShoppingCart cart = new ShoppingCart();
        Product p = new Product("Book", 29.99);

        cart.add(p);

        assertEquals(29.99, cart.getTotal(), 0.001);
    }

    @Test
    void apply10PercentDiscount() {
        ShoppingCart cart = new ShoppingCart();
        cart.add(new Product("A", 100.0));

        cart.applyDiscount(10);

        assertEquals(90.0, cart.getTotal(), 0.001);
    }
}

// STEP 2 - GREEN: Write minimum code to make the test pass
class Product {
    String name;
    double price;

    Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
}

class ShoppingCart {
    private List<Product> products = new ArrayList<>();
    private double discountPercentage = 0;

    void add(Product p) {
        products.add(p);
    }

    double getTotal() {
        double total = products.stream()
            .mapToDouble(p -> p.price)
            .sum();
        return total * (1 - discountPercentage / 100);
    }

    void applyDiscount(double percentage) {
        this.discountPercentage = percentage;
    }
}

// STEP 3 - REFACTOR: Improve the code keeping tests green

Best Practices

Rules for Effective Tests

  • One logical assertion per test: focused tests
  • Descriptive names: describe what they test
  • AAA pattern: Arrange, Act, Assert
  • Independent tests: don't depend on order
  • Fast tests: must be run often
  • Mock only external dependencies: database, API, files
  • Avoid fragile tests: don't test implementation
  • Coverage isn't everything: quality > quantity

Frequently asked questions

What is the standard framework for unit testing in Java?

JUnit.

What does Mockito allow developers to create in their tests?

Mock objects.

What is the purpose of automated tests in software development?

To ensure code quality and maintainability.

What is Test-Driven Development (TDD)?

A development approach where testing is done before writing actual code.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects