Mockito is the most popular mocking framework for Java unit tests. It allows you to create and configure mock objects, verify interactions, and stub method calls, making it easier to test components in isolation.
Dependencies Setup
Maven Dependencies
<properties>
<mockito.version>5.8.0</mockito.version>
<junit.version>5.10.0</junit.version>
</properties>
<dependencies>
<!-- Mockito Core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- Mockito JUnit 5 Integration -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for fluent assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Basic Mockito Usage
Example 1: Basic Mock Creation and Stubbing
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class BasicMockitoTest {
@Mock
private List<String> mockedList;
@Test
void testBasicMockBehavior() {
// Configure mock behavior
when(mockedList.size()).thenReturn(100);
when(mockedList.get(0)).thenReturn("first element");
when(mockedList.get(1)).thenReturn("second element");
// Use the mock
mockedList.add("one");
// Verify interactions and assert behavior
assertEquals(100, mockedList.size());
assertEquals("first element", mockedList.get(0));
assertEquals("second element", mockedList.get(1));
// Verify interactions
verify(mockedList).add("one");
verify(mockedList, times(1)).add("one");
verify(mockedList, atLeastOnce()).size();
verify(mockedList, never()).clear();
}
@Test
void testArgumentMatchers() {
when(mockedList.get(anyInt())).thenReturn("element");
assertEquals("element", mockedList.get(0));
assertEquals("element", mockedList.get(999));
verify(mockedList, times(2)).get(anyInt());
}
@Test
void testVoidMethods() {
// For void methods, use doNothing(), doThrow(), etc.
doThrow(new RuntimeException("Boom!")).when(mockedList).clear();
assertThrows(RuntimeException.class, () -> mockedList.clear());
}
}
Real-World Testing Examples
Example 2: Service Layer Testing
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
// Domain Classes
class User {
private Long id;
private String username;
private String email;
private BigDecimal balance;
public User(Long id, String username, String email, BigDecimal balance) {
this.id = id;
this.username = username;
this.email = email;
this.balance = balance;
}
// Getters and setters
public Long getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { this.balance = balance; }
}
class Transaction {
private Long id;
private Long userId;
private BigDecimal amount;
private String type;
private LocalDateTime timestamp;
public Transaction(Long id, Long userId, BigDecimal amount, String type, LocalDateTime timestamp) {
this.id = id;
this.userId = userId;
this.amount = amount;
this.type = type;
this.timestamp = timestamp;
}
// Getters
public Long getId() { return id; }
public Long getUserId() { return userId; }
public BigDecimal getAmount() { return amount; }
public String getType() { return type; }
public LocalDateTime getTimestamp() { return timestamp; }
}
// Repository Interfaces
interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
}
interface TransactionRepository {
Transaction save(Transaction transaction);
}
// Service Class
class PaymentService {
private final UserRepository userRepository;
private final TransactionRepository transactionRepository;
public PaymentService(UserRepository userRepository, TransactionRepository transactionRepository) {
this.userRepository = userRepository;
this.transactionRepository = transactionRepository;
}
public Transaction processPayment(Long userId, BigDecimal amount) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
if (user.getBalance().compareTo(amount) < 0) {
throw new IllegalArgumentException("Insufficient balance");
}
// Update user balance
user.setBalance(user.getBalance().subtract(amount));
userRepository.save(user);
// Create transaction record
Transaction transaction = new Transaction(
null, userId, amount, "PAYMENT", LocalDateTime.now()
);
return transactionRepository.save(transaction);
}
public BigDecimal getUserBalance(Long userId) {
return userRepository.findById(userId)
.map(User::getBalance)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
}
}
// Test Class
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private TransactionRepository transactionRepository;
private PaymentService paymentService;
@BeforeEach
void setUp() {
paymentService = new PaymentService(userRepository, transactionRepository);
}
@Test
void processPayment_Successful() {
// Arrange
Long userId = 1L;
BigDecimal initialBalance = new BigDecimal("100.00");
BigDecimal paymentAmount = new BigDecimal("50.00");
BigDecimal expectedBalance = new BigDecimal("50.00");
User user = new User(userId, "john_doe", "[email protected]", initialBalance);
Transaction expectedTransaction = new Transaction(1L, userId, paymentAmount, "PAYMENT", LocalDateTime.now());
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(transactionRepository.save(any(Transaction.class))).thenReturn(expectedTransaction);
// Act
Transaction result = paymentService.processPayment(userId, paymentAmount);
// Assert
assertThat(result).isEqualTo(expectedTransaction);
// Verify interactions
verify(userRepository).findById(userId);
verify(userRepository).save(argThat(userArg ->
userArg.getBalance().equals(expectedBalance)));
verify(transactionRepository).save(any(Transaction.class));
}
@Test
void processPayment_UserNotFound() {
// Arrange
Long userId = 999L;
BigDecimal amount = new BigDecimal("50.00");
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> paymentService.processPayment(userId, amount))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("User not found");
verify(userRepository).findById(userId);
verify(userRepository, never()).save(any());
verify(transactionRepository, never()).save(any());
}
@Test
void processPayment_InsufficientBalance() {
// Arrange
Long userId = 1L;
BigDecimal initialBalance = new BigDecimal("30.00");
BigDecimal paymentAmount = new BigDecimal("50.00");
User user = new User(userId, "john_doe", "[email protected]", initialBalance);
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act & Assert
assertThatThrownBy(() -> paymentService.processPayment(userId, paymentAmount))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Insufficient balance");
verify(userRepository).findById(userId);
verify(userRepository, never()).save(any());
verify(transactionRepository, never()).save(any());
}
@Test
void getUserBalance_Success() {
// Arrange
Long userId = 1L;
BigDecimal expectedBalance = new BigDecimal("100.00");
User user = new User(userId, "john_doe", "[email protected]", expectedBalance);
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
BigDecimal result = paymentService.getUserBalance(userId);
// Assert
assertThat(result).isEqualByComparingTo(expectedBalance);
verify(userRepository).findById(userId);
}
}
Advanced Mockito Features
Example 3: Argument Captors and Verification
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class AdvancedMockitoTest {
@Mock
private EmailService emailService;
@Mock
private UserRepository userRepository;
@Captor
private ArgumentCaptor<String> emailCaptor;
@Captor
private ArgumentCaptor<User> userCaptor;
@Test
void testArgumentCaptor() {
// Arrange
String email = "[email protected]";
// Act
emailService.sendEmail(email, "Subject", "Body");
// Assert using argument captor
verify(emailService).sendEmail(emailCaptor.capture(), anyString(), anyString());
String capturedEmail = emailCaptor.getValue();
assertThat(capturedEmail).isEqualTo(email);
}
@Test
void testMultipleArgumentCaptures() {
// Arrange
User user1 = new User(1L, "user1", "[email protected]", new BigDecimal("100.00"));
User user2 = new User(2L, "user2", "[email protected]", new BigDecimal("200.00"));
// Act
userRepository.save(user1);
userRepository.save(user2);
// Assert - capture multiple invocations
verify(userRepository, times(2)).save(userCaptor.capture());
List<User> capturedUsers = userCaptor.getAllValues();
assertThat(capturedUsers).hasSize(2);
assertThat(capturedUsers).extracting(User::getUsername)
.containsExactly("user1", "user2");
}
@Test
void testInOrderVerification() {
// Arrange
UserService userService = new UserService(userRepository, emailService);
User user = new User(1L, "john", "[email protected]", new BigDecimal("100.00"));
when(userRepository.save(any(User.class))).thenReturn(user);
// Act
userService.registerUser("john", "[email protected]");
// Assert - verify calls happened in specific order
InOrder inOrder = inOrder(userRepository, emailService);
inOrder.verify(userRepository).save(any(User.class));
inOrder.verify(emailService).sendWelcomeEmail(anyString());
}
}
// Supporting classes for the test
class EmailService {
public void sendEmail(String to, String subject, String body) {}
public void sendWelcomeEmail(String email) {}
}
class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User registerUser(String username, String email) {
User user = new User(null, username, email, BigDecimal.ZERO);
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(email);
return savedUser;
}
}
Example 4: Mocking with Answers and Callbacks
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class MockitoAnswersTest {
@Mock
private IdGenerator idGenerator;
@Mock
private CacheService cacheService;
@Test
void testThenAnswer() {
// Arrange - use thenAnswer for complex return logic
AtomicInteger counter = new AtomicInteger(1);
when(idGenerator.generateId()).thenAnswer(invocation -> {
int id = counter.getAndIncrement();
return "ID-" + id;
});
// Act & Assert
assertThat(idGenerator.generateId()).isEqualTo("ID-1");
assertThat(idGenerator.generateId()).isEqualTo("ID-2");
assertThat(idGenerator.generateId()).isEqualTo("ID-3");
}
@Test
void testThenReturnMultipleValues() {
// Arrange - return different values on consecutive calls
when(idGenerator.generateId())
.thenReturn("FIRST-ID")
.thenReturn("SECOND-ID")
.thenThrow(new RuntimeException("No more IDs"));
// Act & Assert
assertThat(idGenerator.generateId()).isEqualTo("FIRST-ID");
assertThat(idGenerator.generateId()).isEqualTo("SECOND-ID");
assertThatThrownBy(() -> idGenerator.generateId())
.isInstanceOf(RuntimeException.class)
.hasMessage("No more IDs");
}
@Test
void testThenCallRealMethod() {
// Arrange - call real method on mock
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// Act
spyList.add("real");
when(spyList.size()).thenReturn(100); // Stub the method
// Assert
assertThat(spyList).containsExactly("real");
assertThat(spyList.size()).isEqualTo(100); // Stubbed value
}
@Test
void testDoAnswerForVoidMethods() {
// Arrange - use doAnswer for void methods
List<String> capturedMessages = new ArrayList<>();
doAnswer(invocation -> {
String message = invocation.getArgument(0);
capturedMessages.add("Processed: " + message);
return null;
}).when(cacheService).processMessage(anyString());
// Act
cacheService.processMessage("Hello");
cacheService.processMessage("World");
// Assert
assertThat(capturedMessages).containsExactly(
"Processed: Hello",
"Processed: World"
);
}
}
// Supporting classes
class IdGenerator {
public String generateId() {
return "default-id";
}
}
class CacheService {
public void processMessage(String message) {
// Default implementation
}
}
Integration with Spring Boot
Example 5: Spring Boot Service Testing
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
// Entity Classes
@Entity
class Product {
@Id
private Long id;
private String name;
private String description;
private Double price;
private Integer stockQuantity;
// Constructors, getters, setters
public Product() {}
public Product(Long id, String name, String description, Double price, Integer stockQuantity) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.stockQuantity = stockQuantity;
}
public Long getId() { return id; }
public String getName() { return name; }
public String getDescription() { return description; }
public Double getPrice() { return price; }
public Integer getStockQuantity() { return stockQuantity; }
public void setStockQuantity(Integer stockQuantity) { this.stockQuantity = stockQuantity; }
}
// Repository
interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByPriceLessThan(Double maxPrice);
Optional<Product> findByName(String name);
}
// Service
@Service
class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> getAffordableProducts(Double maxPrice) {
return productRepository.findByPriceLessThan(maxPrice);
}
public Product updateStock(Long productId, Integer newStock) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException("Product not found"));
product.setStockQuantity(newStock);
return productRepository.save(product);
}
public Product createProduct(Product product) {
// Check if product with same name exists
if (productRepository.findByName(product.getName()).isPresent()) {
throw new DuplicateProductException("Product with this name already exists");
}
return productRepository.save(product);
}
}
// Custom Exceptions
class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) { super(message); }
}
class DuplicateProductException extends RuntimeException {
public DuplicateProductException(String message) { super(message); }
}
// Test Class
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
void getAffordableProducts_ShouldReturnFilteredProducts() {
// Arrange
Double maxPrice = 50.0;
List<Product> affordableProducts = List.of(
new Product(1L, "Product A", "Description A", 25.0, 10),
new Product(2L, "Product B", "Description B", 35.0, 5)
);
when(productRepository.findByPriceLessThan(maxPrice)).thenReturn(affordableProducts);
// Act
List<Product> result = productService.getAffordableProducts(maxPrice);
// Assert
assertThat(result).hasSize(2);
assertThat(result).extracting(Product::getPrice)
.allMatch(price -> price < maxPrice);
verify(productRepository).findByPriceLessThan(maxPrice);
}
@Test
void updateStock_ProductExists_ShouldUpdateStock() {
// Arrange
Long productId = 1L;
Integer newStock = 50;
Product existingProduct = new Product(productId, "Product A", "Desc", 25.0, 10);
Product updatedProduct = new Product(productId, "Product A", "Desc", 25.0, newStock);
when(productRepository.findById(productId)).thenReturn(Optional.of(existingProduct));
when(productRepository.save(any(Product.class))).thenReturn(updatedProduct);
// Act
Product result = productService.updateStock(productId, newStock);
// Assert
assertThat(result.getStockQuantity()).isEqualTo(newStock);
verify(productRepository).findById(productId);
verify(productRepository).save(argThat(product ->
product.getStockQuantity().equals(newStock)));
}
@Test
void updateStock_ProductNotFound_ShouldThrowException() {
// Arrange
Long productId = 999L;
when(productRepository.findById(productId)).thenReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> productService.updateStock(productId, 50))
.isInstanceOf(ProductNotFoundException.class)
.hasMessage("Product not found");
verify(productRepository).findById(productId);
verify(productRepository, never()).save(any());
}
@Test
void createProduct_WithUniqueName_ShouldCreateProduct() {
// Arrange
Product newProduct = new Product(null, "New Product", "Description", 99.99, 10);
Product savedProduct = new Product(1L, "New Product", "Description", 99.99, 10);
when(productRepository.findByName("New Product")).thenReturn(Optional.empty());
when(productRepository.save(newProduct)).thenReturn(savedProduct);
// Act
Product result = productService.createProduct(newProduct);
// Assert
assertThat(result.getId()).isEqualTo(1L);
verify(productRepository).findByName("New Product");
verify(productRepository).save(newProduct);
}
@Test
void createProduct_WithDuplicateName_ShouldThrowException() {
// Arrange
Product newProduct = new Product(null, "Existing Product", "Description", 99.99, 10);
Product existingProduct = new Product(1L, "Existing Product", "Description", 99.99, 10);
when(productRepository.findByName("Existing Product")).thenReturn(Optional.of(existingProduct));
// Act & Assert
assertThatThrownBy(() -> productService.createProduct(newProduct))
.isInstanceOf(DuplicateProductException.class)
.hasMessage("Product with this name already exists");
verify(productRepository).findByName("Existing Product");
verify(productRepository, never()).save(any());
}
}
Mockito Best Practices
Example 6: Best Practices and Common Patterns
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class MockitoBestPracticesTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
private UserService userService;
@BeforeEach
void setUp() {
userService = new UserService(userRepository, emailService);
}
@Test
void shouldUseDescriptiveTestNames() {
// Test names should describe the behavior being tested
when(userRepository.findAll()).thenReturn(Collections.emptyList());
assertThat(userService.getAllUsers()).isEmpty();
}
@Test
void shouldVerifyImportantInteractions() {
// Only verify interactions that are important for the test
User user = new User(1L, "test", "[email protected]", new BigDecimal("100.00"));
when(userRepository.save(any(User.class))).thenReturn(user);
userService.registerUser("test", "[email protected]");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("[email protected]");
}
@Test
void shouldNotOverSpecify() {
// Don't verify every single interaction if it's not important
User user = new User(1L, "test", "[email protected]", new BigDecimal("100.00"));
when(userRepository.save(any(User.class))).thenReturn(user);
userService.registerUser("test", "[email protected]");
// Good: Verify important business logic
verify(emailService).sendWelcomeEmail("[email protected]");
// Avoid: Over-verifying implementation details
// verify(userRepository, times(1)).save(any(User.class));
// verifyNoMoreInteractions(userRepository);
}
@Test
void shouldUseAppropriateArgumentMatchers() {
// Use specific matchers when possible
when(userRepository.findByName(eq("john"))).thenReturn(Optional.empty());
// Use any() only when the specific value doesn't matter
when(userRepository.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0));
userService.registerUser("john", "[email protected]");
verify(userRepository).findByName("john");
}
@Test
void shouldResetMocksBetweenTestClasses() {
// In integration tests, you might need to reset mocks
// This is usually handled by the test framework
}
@Test
void shouldAvoidMockingValueObjects() {
// Don't mock simple value objects or data classes
// Instead, create real instances
// Good: Use real objects for simple data carriers
User realUser = new User(1L, "john", "[email protected]", new BigDecimal("100.00"));
// Avoid: Mocking simple data objects
// User mockedUser = mock(User.class);
// when(mockedUser.getName()).thenReturn("john");
}
}
// Additional supporting class
class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User registerUser(String username, String email) {
// Check if user exists
userRepository.findByName(username).ifPresent(user -> {
throw new IllegalArgumentException("User already exists");
});
User user = new User(null, username, email, BigDecimal.ZERO);
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(email);
return savedUser;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
Common Mockito Patterns
1. Given-When-Then Pattern
@Test
void givenUserExists_whenGetUser_thenReturnUser() {
// Given
User expectedUser = new User(1L, "john", "[email protected]", new BigDecimal("100.00"));
when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
// When
User result = userService.getUser(1L);
// Then
assertThat(result).isEqualTo(expectedUser);
}
2. Behavior Verification
@Test
void shouldSendEmailAfterUserRegistration() {
// When
userService.registerUser("john", "[email protected]");
// Then
verify(emailService).sendWelcomeEmail("[email protected]");
}
3. Exception Testing
@Test
void shouldThrowExceptionWhenUserNotFound() {
when(userRepository.findById(999L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.getUser(999L))
.isInstanceOf(UserNotFoundException.class);
}
Conclusion
Mockito is an essential tool for Java developers writing unit tests. Key takeaways:
- Isolate Dependencies: Mock external dependencies to test components in isolation
- Flexible Stubbing: Configure mock behavior with
when().thenReturn()and related methods - Verification: Ensure proper interactions with dependencies using
verify() - Argument Matching: Use argument matchers for flexible verification
- Best Practices: Follow testing best practices for maintainable, readable tests
Mockito integrates well with JUnit 5 and Spring Boot, making it the go-to choice for mocking in modern Java applications. Remember to use mocks judiciously and focus on testing behavior rather than implementation details.