Advanced Java

Testing with Mockito

Bonus Session - Advanced Mocking and Verification

Session Objectives

By the end of this bonus session, you will:

Prerequisites: Complete Module 2 (Dependency Injection) first

Why Mockito?

While manual test doubles work well, Mockito provides powerful features for complex scenarios:

Adding Mockito to Your Project

Add Mockito dependency to your build file:

Maven:

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

Gradle:

testImplementation(
    'org.mockito:mockito-junit-jupiter:5.14.2'
)

Using Mockito Annotations

@ExtendWith(MockitoExtension.class) // (#1:JUnit 5 extension for Mockito support)
class UserServiceTest {

    @Mock // (#2:Create mock instances)
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks  // (#3:Automatically injects @Mock dependencies into UserService)
    private UserService userService;

    @Test
    void shouldCreateUserAndSendEmail() {
        User user = new User("test@example.com");

        when(userRepository.save(any(User.class))) // (#4:Stub the mock behavior)
            .thenReturn(user);

        userService.createUser(user);

        verify(userRepository).save(user); // (#5:Verify method was called)
        verify(emailService).sendWelcomeEmail("test@example.com");
    }
}

Creating Mocks Programmatically

class UserServiceTest {

    @Test
    void shouldCreateUserWithManualMocks() {
        // Create mocks manually (alternative to annotations)
        UserRepository mockRepo = mock(UserRepository.class); // (#1:Create mock)
        EmailService mockEmail = mock(EmailService.class);

        // Create service with mocks
        UserService service = new UserService(mockRepo, mockEmail);

        User user = new User("john@example.com");
        when(mockRepo.save(any(User.class))).thenReturn(user); // (#2:Define behavior)

        // Act
        service.createUser(user);

        // Assert
        verify(mockRepo).save(user); // (#3:Verify interaction)
    }
}

Use annotations for cleaner tests, manual creation for one-off scenarios

Stubbing: Defining Mock Behavior

@Test
void stubbingExamples() {
    // Return a value
    when(userRepository.findById(1L))
        .thenReturn(Optional.of(new User("john@example.com"))); // (#1:Return value)

    // Return different values on consecutive calls
    when(userRepository.count())
        .thenReturn(5L)
        .thenReturn(10L)
        .thenReturn(15L); // (#2:Different returns each call)

    // Throw an exception
    when(userRepository.findById(-1L))
        .thenThrow(new IllegalArgumentException("Invalid ID")); // (#3:Throw exception)

    // Answer dynamically based on arguments
    when(userRepository.findByEmail(anyString()))
        .thenAnswer(invocation -> { // (#4:Dynamic response)
            String email = invocation.getArgument(0);
            return Optional.of(new User(email));
        });
}

Argument Matchers

@Test
void argumentMatcherExamples() {
    // Match any value of a type
    when(userRepository.save(any(User.class))) // (#1:Any User object)
        .thenReturn(new User("saved@example.com"));

    // Match specific conditions
    when(userRepository.findByEmail(argThat(email ->
        email.endsWith("@company.com")))) // (#2:Custom matcher)
        .thenReturn(Optional.of(new User("employee")));

    // Match null or non-null
    when(userRepository.save(isNull())).thenThrow(new IllegalArgumentException());
    when(userRepository.save(isNotNull())).thenReturn(new User()); // (#3:Null checks)

    // Match by equality
    when(userRepository.findById(eq(42L))) // (#4:Exact match)
        .thenReturn(Optional.of(new User("answer@universe.com")));
}

Rule: If using any matcher, ALL arguments must use matchers!

Verifying Method Calls

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

    User user = new User("test@example.com");
    service.createUser(user);

    // Verify exact call
    verify(mockRepo).save(user); // (#1:Was save() called with this user?)

    // Verify number of calls
    verify(mockRepo, times(1)).save(any()); // (#2:Exactly once)

    // Verify no other interactions
    verifyNoMoreInteractions(mockRepo); // (#3:No unexpected calls)

    // Verify never called
    verify(mockRepo, never()).delete(any()); // (#4:Never called)

    // Verify call order
    InOrder inOrder = inOrder(mockRepo);
    inOrder.verify(mockRepo).save(user); // (#5:Order matters)
}

Verification Modes

Mode Description
times(n) Exactly n invocations
never() Zero invocations (same as times(0))
atLeastOnce() At least one invocation
atLeast(n) At least n invocations
atMost(n) At most n invocations
only() Only this method was called

Capturing Arguments

@ExtendWith(MockitoExtension.class)
class EmailServiceTest {

    @Mock
    private EmailSender emailSender;

    @InjectMocks
    private NotificationService notificationService;

    @Captor // (#1:Declare captor with annotation)
    private ArgumentCaptor<Email> emailCaptor;

    @Test
    void shouldSendCorrectEmail() {
        notificationService.notifyUser("user@example.com", "Welcome");

        // Capture the argument passed to emailSender
        verify(emailSender).send(emailCaptor.capture()); // (#2:Capture during verify)

        Email sentEmail = emailCaptor.getValue(); // (#3:Get captured value)
        assertEquals("user@example.com", sentEmail.getTo());
        assertEquals("Welcome", sentEmail.getSubject());
        assertTrue(sentEmail.getBody().contains("Welcome"));
    }
}

Capturing Multiple Invocations

@Test
void shouldCaptureAllEmails() {
    // Trigger multiple notifications
    notificationService.notifyUser("user1@example.com", "Hello");
    notificationService.notifyUser("user2@example.com", "Welcome");
    notificationService.notifyUser("user3@example.com", "Goodbye");

    // Capture all invocations
    verify(emailSender, times(3)).send(emailCaptor.capture()); // (#1:Verify 3 calls)

    // Get all captured values
    List<Email> allEmails = emailCaptor.getAllValues(); // (#2:Get all captures)

    assertEquals(3, allEmails.size());
    assertEquals("user1@example.com", allEmails.get(0).getTo());
    assertEquals("user2@example.com", allEmails.get(1).getTo());
    assertEquals("user3@example.com", allEmails.get(2).getTo());
}

Understanding @Spy vs @Mock

class SpyVsMockTest {

    @Mock
    private List<String> mockList;  // Completely fake - no real behavior

    @Spy
    private List<String> spyList = new ArrayList<>();  // Real object, partial mock

    @Test
    void demonstrateDifference() {
        // Mock - all methods return defaults (null, 0, false, empty)
        mockList.add("one");
        assertEquals(0, mockList.size());  // (#1:Still 0! No real behavior)

        // Spy - real methods called unless explicitly stubbed
        spyList.add("one");
        assertEquals(1, spyList.size());  // (#2:Actually adds element)

        // Can stub specific methods on spy
        when(spyList.size()).thenReturn(100); // (#3:Override specific method)
        assertEquals(100, spyList.size());  // Returns 100
        assertEquals("one", spyList.get(0)); // (#4:Real behavior still works)
    }
}

When to Use @Spy

Use @Spy When:

Use @Mock When:

doReturn() vs when() for Spies

@Test
void doReturnVsWhen() {
    List<String> spy = spy(new ArrayList<>());

    // WRONG: when() calls the real method first!
    // when(spy.get(0)).thenReturn("element");  // Throws IndexOutOfBoundsException!

    // CORRECT: doReturn() doesn't call the real method
    doReturn("element").when(spy).get(0); // (#1:Safe for spies)
    assertEquals("element", spy.get(0));

    // For throwing exceptions
    doThrow(new RuntimeException("Error"))
        .when(spy).clear(); // (#2:Override void methods)

    // For void methods that do nothing
    doNothing().when(spy).clear(); // (#3:Suppress real behavior)
}

Rule: Always use doReturn() with spies to avoid calling real methods

BDD Style with Mockito

import static org.mockito.BDDMockito.*;

@Test
void shouldCreateUserBDDStyle() {
    // Given (setup)
    User user = new User("test@example.com");
    given(userRepository.save(any(User.class))) // (#1:BDD given - replaces when)
        .willReturn(user);

    // When (action)
    User created = userService.createUser(user);

    // Then (verification)
    then(userRepository) // (#2:BDD then - replaces verify)
        .should()
        .save(user);

    then(emailService)
        .should(times(1))
        .sendWelcomeEmail("test@example.com"); // (#3:Verification with mode)

    assertThat(created.getEmail()).isEqualTo("test@example.com");
}

BDD style makes tests more readable with Given-When-Then structure

Mocking Static Methods (Mockito 5+)

@Test
void shouldMockStaticMethods() {
    try (MockedStatic<UUID> mockedUUID = mockStatic(UUID.class)) { // (#1:Static mock scope)

        UUID fixedUUID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");

        mockedUUID.when(UUID::randomUUID) // (#2:Mock static method)
            .thenReturn(fixedUUID);

        // Test code that uses UUID.randomUUID()
        String id = entityService.generateId();

        assertEquals("123e4567-e89b-12d3-a456-426614174000", id);
    } // (#3:Static mock is closed automatically - real behavior restored)
}

Warning: Static mocking is a code smell. Prefer dependency injection!

Strict Stubbing Mode

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS) // (#1:Enable strict mode)
class StrictTest {

    @Mock
    private UserRepository userRepository;

    @Test
    void strictStubbingPreventsUnusedStubs() {
        // This test will FAIL if you stub something you don't use
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(new User("test"))); // (#2:Must be used!)

        // If we don't call findById(1L), test fails with:
        // "Unnecessary stubbings detected"

        User user = userRepository.findById(1L).get(); // (#3:Stub is used - OK)
        assertEquals("test", user.getEmail());
    }
}

Strict stubbing helps identify dead code and copy-paste errors

Mockito Best Practices

Common Anti-Patterns to Avoid

1. Over-mocking

// BAD: Mocking everything
when(user.getEmail()).thenReturn("test@example.com");
when(user.getName()).thenReturn("John");

// GOOD: Use real objects
User user = new User("test@example.com", "John");

2. Verifying Getters

// BAD: Verifying obvious calls
verify(userRepository).save(user);
verify(user).getEmail(); // Pointless!

// GOOD: Verify meaningful interactions
verify(emailService).sendWelcomeEmail("test@example.com");

When NOT to Use Mocks

Don't Mock:

Consider Fakes Instead:

Practice Exercise

Task: Write tests for an OrderService with mocks

public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final NotificationService notificationService;

    public Order placeOrder(Order order) {
        Order saved = orderRepository.save(order);
        paymentService.processPayment(order.getTotal());
        notificationService.sendOrderConfirmation(order.getCustomerEmail());
        return saved;
    }
}

Requirements:

  1. Use @Mock and @InjectMocks
  2. Verify all three services are called
  3. Use ArgumentCaptor for the notification email
  4. Test exception handling when payment fails

Summary

Concept Usage
@Mock Create fake object with no behavior
@Spy Wrap real object, override specific methods
@InjectMocks Auto-inject mocks into class under test
when().thenReturn() Define mock behavior
verify() Check method was called
@Captor Capture arguments for assertions

Key Insight: Mocks are for isolation. Use them wisely, not everywhere!

Resources

Slide Overview