Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 74

Mocking Dependencies with Mockito

Recommended resources

Hello again! In our last lesson, you wrote your first unit tests and saw how crucial it is to isolate the component under test from its dependencies. You used Mockito's @Mock and @InjectMocks annotations to replace a real repository with a "fake" one, allowing you to test your service's logic in a controlled environment.

Today, we'll dive much deeper into the power of Mockito. You'll move beyond simply telling a mock what to return and learn how to verify its behavior—did your service interact with its dependencies correctly? You'll also learn how to test more complex scenarios, such as error conditions and exception handling. Mastering these techniques is fundamental for building the robust, maintainable backend applications that are the cornerstone of enterprise Java development.

By the end of this lesson, you will be able to use Mockito's full capabilities to stub various behaviors, verify method interactions, and write comprehensive unit tests for your service layer.

The Two Sides of Testing: State vs. Behavior

In the previous lesson, your tests focused on state-based testing. You set up a condition, called a method, and then asserted something about the state of the result. For example, you asserted that the returned list of users had a size of 2.

// State-based test: We check the final state of the returned list.
List<User> userList = userService.getAllUsers();
assertThat(userList.size()).isEqualTo(2);

This is essential, but it only tells half the story. What if you need to confirm that a delete method, which returns void, actually called the repository's delete function? This is where behavior-based testing comes in. With behavior testing, we verify that the method under test performed the correct actions or interactions with its dependencies.

Mockito is a powerful framework that excels at both. Let's look at the core components of how you can use it to create sophisticated tests.

Unit tests with Mockito - Tutorial

This article from vogella provides a great introduction to Mockito's philosophy.

Read the section "What are mock objects?" for a quick refresher. Then, focus on the section State testing vs behavior testing. This will solidify the conceptual difference between checking a result (state) and checking if a method was called (behavior).


Deeper Control: Advanced Stubbing

In the last lesson, you used when(...).thenReturn(...) to stub a simple return value. This is the most common form of stubbing, but Mockito gives you more granular control.

Stubbing Exceptions

What happens in your UserService if the repository throws a DatabaseConnectionException? Your application should handle this gracefully. Mockito makes it easy to simulate such error conditions. Instead of using thenReturn(), you use thenThrow().

Let's imagine your createUser method should throw a custom UserCreationException if the repository fails. You can test this logic without needing a real database failure.

@Test
public void givenRepositoryThrowsException_whenCreateUser_thenServiceShouldThrowCustomException() {
    // Given: We configure the mock repository to throw an exception when save() is called.
    // We can use any() as a matcher because we don't care about the specific user object here.
    when(userRepository.save(any(User.class)))
        .thenThrow(new DataAccessException("Database is down"));

    // When & Then: We assert that calling our service method throws the expected custom exception.
    assertThrows(UserCreationException.class, () -> {
        userService.createUser(new User("test@example.com", "Test User"));
    });
}

This test isolates your service's exception handling logic. You are testing that your service correctly catches the DataAccessException and translates it into a UserCreationException, which is a critical piece of business logic.

The Djamware and vogella articles both provide excellent examples of this pattern.

Unit Testing in Spring Boot with JUnit and Mockito - Djamware

This article from Djamware offers a concise, practical look at testing exceptions.

Review the code in the section "Testing Exceptions with Mockito". Notice how it combines when(...).thenReturn(Optional.empty()) with assertThrows to test the "not found" scenario. The principle is the same as using thenThrow.


Verifying Behavior: Was the Method Called?

Verifying interactions is the core of behavior-based testing. Mockito's verify() method lets you check if a method on a mock object was called, how many times, and with what arguments.

The basic syntax is verify(mockObject).methodName(arguments). By default, this checks for exactly one invocation.

Let's test our deleteUser service method. It returns void, so we can't assert a return value. Instead, we verify that it called the repository's deleteById method correctly.

@Test
public void whenDeleteUser_thenRepositoryDeleteShouldBeCalled() {
    // Given
    Long userId = 1L;

    // When
    userService.deleteUser(userId);

    // Then: Verify that the deleteById method on the userRepository mock
    // was called exactly once with the argument 1L.
    verify(userRepository).deleteById(userId);
}

Verification with More Precision

Mockito provides modifiers to be more specific about the interaction:

  • times(n): Verifies the method was called exactly n times. verify(mock, times(2)).someMethod();
  • never(): An alias for times(0). Useful for ensuring an action didn't happen. verify(mock, never()).someMethod();
  • atLeast(n) / atMost(n): Verifies the invocation count is within a range.
  • verifyNoMoreInteractions(mock): A strict check that asserts no other methods were called on the mock besides those you've already verified.

Let's revisit our "email already exists" test case. When a user tries to register with an existing email, not only should an exception be thrown, but the save method should never be called.

@Test
public void givenEmailExists_whenCreateUser_thenSaveShouldNeverBeCalled() {
    // Given
    User user = new User("existing@example.com", "Existing User");
    when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);

    // When
    // We expect an exception, so we can wrap the call in an assertThrows block
    assertThrows(IllegalArgumentException.class, () -> {
        userService.createUser(user);
    });

    // Then
    // Verify that the save method was NEVER called.
    verify(userRepository, never()).save(any(User.class));
}

This video from Ali Bouali provides a fantastic, step-by-step demonstration of both stubbing and verification in a real-world context.

🚀 Master Unit Testing in Spring Boot with JUnit 5 & Mockito | Complete Guide 2025

This section of the "Master Unit Testing" video walks through creating a complete unit test for a create method, showcasing stubbing, action, and verification.

Watch from the beginning of this clip until the end. Pay close attention to how he: Stubs multiple dependencies (categoryRepository, toDoMapper, toDoRepository) using when().thenReturn(). Verifies that each of these dependencies was called correctly using verify() and times(1).


Advanced Verification: Argument Matchers and Captors

What if you want to verify a method was called, but you don't know the exact value of the argument? Or what if you need to inspect the argument itself for more complex assertions? Mockito provides two powerful tools for this: Argument Matchers and Argument Captors.

Argument Matchers

Matchers are used in both stubbing and verification to specify flexible argument criteria.

  • any(): Matches any object, including null.
  • any(Class.class): Matches any object of a specific type (e.g., any(User.class)).
  • anyString(), anyInt(), etc.: Matches any value of a primitive wrapper type.
  • eq(value): Matches a specific value. This is useful when you mix matchers with literal values.

A critical rule: If you use an argument matcher for one parameter in a method call, you must use matchers for all parameters.

// GOOD: All arguments use matchers
when(userRepository.findUser(anyString(), anyInt())).thenReturn(user);

// BAD: Mixing a matcher with a literal value
// when(userRepository.findUser(anyString(), 25)).thenReturn(user); // This will fail!

// CORRECT way to mix: Use the eq() matcher for the literal value
when(userRepository.findUser(anyString(), eq(25))).thenReturn(user);

ArgumentCaptor

Sometimes, you need to perform assertions on the object that was passed to your mock. For instance, your createUser method might set a createdAt timestamp on the User object right before passing it to userRepository.save(). How do you test that the timestamp was set correctly?

An ArgumentCaptor allows you to "capture" the argument for later inspection.

This diagram illustrates the core testing setup. Our `ClassA` (e.g., `UserService`) is the class under test. It depends on `ClassB` (e.g., `UserRepository`). In our `ClassATest`, we use `@Mock` to create a fake `ClassB` and `@InjectMocks` to create a real `ClassA` with the fake dependency injected.

Here's how you'd use an ArgumentCaptor to test the createdAt timestamp:

// 1. Declare the captor for the type you want to capture
@Captor
ArgumentCaptor<User> userArgumentCaptor;

// Note: You can also create it programmatically:
// ArgumentCaptor<User> userArgumentCaptor = ArgumentCaptor.forClass(User.class);

@Test
public void whenCreateUser_thenUserObjectShouldHaveCreationDate() {
    // Given
    User userToCreate = new User("new@example.com", "New User");
    when(userRepository.existsByEmail(anyString())).thenReturn(false);
    when(userRepository.save(any(User.class))).thenReturn(userToCreate);

    // When
    userService.createUser(userToCreate);

    // Then
    // 2. Capture the argument during verification
    verify(userRepository).save(userArgumentCaptor.capture());

    // 3. Get the captured value
    User capturedUser = userArgumentCaptor.getValue();

    // 4. Assert on the captured object
    assertThat(capturedUser.getCreatedAt()).isNotNull();
    assertThat(capturedUser.getCreatedAt()).isCloseTo(LocalDateTime.now(), within(1, ChronoUnit.SECONDS));
}

This is a very powerful technique for verifying logic that modifies objects before they are passed to dependencies.

Unit tests with Mockito - Tutorial

This vogella tutorial provides clear examples for both argument matchers and captors.

First, read the section Argument verification and capturing. It shows how to use argThat (a custom matcher) and a basic ArgumentCaptor. Then, read the dedicated section Capturing the arguments for a more focused example of using ArgumentCaptor.


Your Turn: Apply What You've Learned

It's time to put these concepts into practice. In your project, navigate to your UserServiceTest class.

  1. Test the deleteUser method:

    • Create a new test method named givenUserId_whenDeleteUser_thenRepositoryDeleteIsCalled.
    • In the "When" part, call userService.deleteUser() with a sample ID (e.g., 1L).
    • In the "Then" part, use verify(userRepository, times(1)).deleteById(1L) to confirm that the repository's deleteById method was invoked exactly once with the correct ID.
  2. Test the updateUser method with a non-existent user:

    • Create a test named givenNonExistentUserId_whenUpdateUser_thenThrowNotFoundException.
    • Given: Stub the userRepository.findById(id) method to return Optional.empty().
    • When/Then: Use assertThrows to verify that a UserNotFoundException (or similar) is thrown when you call userService.updateUser(...).
    • Then (Verification): Use verify(userRepository, never()).save(any(User.class)) to ensure that if the user doesn't exist, the save method is never called.
A typical view in an IDE after running unit tests. The green checkmarks indicate that all tests, including those for success paths and failure scenarios, have passed, giving you confidence in your code's logic.

Conclusion

In this lesson, you've significantly expanded your unit testing toolkit. You've moved from simple state-based checks to sophisticated behavior-based verification using Mockito.

Key Takeaways:

  • State vs. Behavior: You can test both the outcome of a method (state) and the interactions it has with its dependencies (behavior).
  • Stubbing Exceptions: Use when(...).thenThrow(...) to test your code's error-handling paths in a controlled way.
  • Verifying Interactions: The verify() method is your primary tool for behavior testing. Use times(), never(), and other modifiers for precision.
  • Argument Matchers & Captors: Use matchers (any(), eq()) for flexible verification and captors (ArgumentCaptor) to inspect the exact arguments passed to your mocks.

You now have a solid foundation for writing professional-grade unit tests that cover not just the "happy path" but also edge cases, error conditions, and complex interactions. In our next lesson, we will move up the testing pyramid to integration testing. You will learn how to test your REST controllers using MockMvc to simulate HTTP requests and verify that your API endpoints behave as expected.