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 exactlyntimes.verify(mock, times(2)).someMethod();never(): An alias fortimes(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.

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.
-
Test the
deleteUsermethod:- 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'sdeleteByIdmethod was invoked exactly once with the correct ID.
- Create a new test method named
-
Test the
updateUsermethod with a non-existent user:- Create a test named
givenNonExistentUserId_whenUpdateUser_thenThrowNotFoundException. - Given: Stub the
userRepository.findById(id)method to returnOptional.empty(). - When/Then: Use
assertThrowsto verify that aUserNotFoundException(or similar) is thrown when you calluserService.updateUser(...). - Then (Verification): Use
verify(userRepository, never()).save(any(User.class))to ensure that if the user doesn't exist, thesavemethod is never called.
- Create a test named

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. Usetimes(),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.