Master Java Mocking with Mockito and Test Doubles
Mocking in Java Testing
Mocking is a technique used in unit testing to replace dependencies with fake objects that simulate the behavior of real components. This allows testing units in isolation by controlling the behavior of their dependencies.
Table of Contents
- Mocking Fundamentals
- Mockito Framework
- Creating Mocks
- Stubbing Behavior
- Verification
- Argument Matchers
- Advanced Mocking
- Best Practices
Mocking Fundamentals
Why Use Mocks?
Isolation: Test units without dependencies Control: Define exact behavior of dependencies Speed: Avoid slow operations (database, network) Reliability: Eliminate external system failures
Types of Test Doubles
// Dummy - Objects passed but never used
public class DummyEmailService implements EmailService {
public void sendEmail(String to, String subject, String body) {
// Does nothing
}
}
// Stub - Provides predefined responses
public class StubPaymentService implements PaymentService {
public PaymentResult processPayment(PaymentRequest request) {
return new PaymentResult("stub-txn-123", PaymentStatus.SUCCESS);
}
}
// Spy - Partial mock of real object
PaymentService spy = Mockito.spy(new RealPaymentService());
// Mock - Fully controlled fake object
PaymentService mock = Mockito.mock(PaymentService.class);
// Fake - Working implementation with shortcuts
public class FakeUserRepository implements UserRepository {
private Map<Long, User> users = new HashMap<>();
public User save(User user) {
users.put(user.getId(), user);
return user;
}
}
Mockito Framework
Setup Dependencies
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Basic Mockito Usage
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class OrderServiceTest {
@Mock
private PaymentService paymentService;
@Mock
private InventoryService inventoryService;
@Mock
private EmailService emailService;
@InjectMocks
private OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void shouldCreateOrderSuccessfully() {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setCustomerId(1L);
request.addItem(new OrderItem("product1", 2, BigDecimal.valueOf(25.00)));
when(inventoryService.checkAvailability("product1", 2)).thenReturn(true);
when(paymentService.processPayment(any(PaymentRequest.class)))
.thenReturn(new PaymentResult("txn-123", PaymentStatus.SUCCESS));
// When
Order result = orderService.createOrder(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
verify(inventoryService).checkAvailability("product1", 2);
verify(paymentService).processPayment(any(PaymentRequest.class));
verify(emailService).sendOrderConfirmation(any(Order.class));
}
}
Creating Mocks
Mock Creation Methods
class MockCreationExamples {
// Method 1: Using @Mock annotation
@Mock
private UserRepository userRepository;
// Method 2: Programmatic creation
@Test
void testWithProgrammaticMock() {
UserRepository userRepository = mock(UserRepository.class);
when(userRepository.findById(1L)).thenReturn(new User("John", "[email protected]"));
// Test implementation
}
// Method 3: Using MockitoExtension (JUnit 5)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldFindUserById() {
// Test implementation
}
}
// Method 4: Manual initialization
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
emailService = mock(EmailService.class);
userService = new UserService(userRepository, emailService);
}
}
Mock Configuration
class MockConfigurationTest {
@Test
void shouldConfigureMockBehavior() {
// Create mock with name for better error messages
UserRepository userRepository = mock(UserRepository.class, "userRepository");
// Create mock with default answer
PaymentService paymentService = mock(PaymentService.class, RETURNS_DEEP_STUBS);
// Create mock with custom answer
EmailService emailService = mock(EmailService.class, invocation -> {
if (invocation.getMethod().getName().equals("sendEmail")) {
return true; // Always return success
}
return null;
});
// Create mock with builder
UserService userService = mock(UserService.class, withSettings()
.name("userService")
.defaultAnswer(RETURNS_SMART_NULLS)
.verboseLogging());
}
}
Stubbing Behavior
Basic Stubbing
class StubbingExamplesTest {
@Mock
private UserRepository userRepository;
@Test
void shouldStubBasicBehavior() {
User expectedUser = new User("John", "[email protected]");
// Simple stubbing
when(userRepository.findById(1L)).thenReturn(expectedUser);
// Multiple return values
when(userRepository.count())
.thenReturn(5L)
.thenReturn(6L)
.thenReturn(7L);
// Exception throwing
when(userRepository.findById(999L))
.thenThrow(new UserNotFoundException("User not found"));
// Void method stubbing
doNothing().when(userRepository).delete(any(User.class));
doThrow(new RuntimeException("Delete failed"))
.when(userRepository).delete(argThat(user -> user.getId() == 999L));
}
@Test
void shouldStubWithCallbacks() {
// Answer interface for complex behavior
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(1L); // Simulate ID generation
user.setCreatedAt(LocalDateTime.now());
return user;
});
// Real method call
User realUser = new User("Real", "[email protected]");
when(userRepository.findByEmail("[email protected]")).thenCallRealMethod();
}
@Test
void shouldStubConditionally() {
// Conditional stubbing based on arguments
when(userRepository.findById(longThat(id -> id > 0 && id <= 1000)))
.thenReturn(new User("Valid User", "[email protected]"));
when(userRepository.findById(longThat(id -> id > 1000)))
.thenThrow(new UserNotFoundException("User ID too large"));
// Stubbing with argument capture
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
when(userRepository.save(userCaptor.capture())).thenAnswer(invocation -> {
User capturedUser = userCaptor.getValue();
capturedUser.setId(123L);
return capturedUser;
});
}
}
Advanced Stubbing
class AdvancedStubbingTest {
@Mock
private PaymentService paymentService;
@Test
void shouldUseAdvancedStubbing() {
// Partial stubbing - some methods real, some mocked
PaymentService partialMock = spy(new RealPaymentService());
doReturn(new PaymentResult("mocked-txn", PaymentStatus.SUCCESS))
.when(partialMock).processPayment(any());
// Consecutive calls with different behaviors
when(paymentService.processPayment(any()))
.thenReturn(new PaymentResult("txn1", PaymentStatus.SUCCESS))
.thenReturn(new PaymentResult("txn2", PaymentStatus.FAILED))
.thenThrow(new PaymentException("Service unavailable"));
// Delayed responses
when(paymentService.processPayment(any())).thenAnswer(invocation -> {
Thread.sleep(1000); // Simulate network delay
return new PaymentResult("delayed-txn", PaymentStatus.SUCCESS);
});
// Reset mock behavior
reset(paymentService);
when(paymentService.processPayment(any()))
.thenReturn(new PaymentResult("reset-txn", PaymentStatus.SUCCESS));
}
@Test
void shouldStubGenericMethods() {
@SuppressWarnings("unchecked")
Repository<User> repository = mock(Repository.class);
when(repository.findAll()).thenReturn(Arrays.asList(
new User("User1", "[email protected]"),
new User("User2", "[email protected]")
));
when(repository.findById(any())).thenReturn(Optional.of(new User("Found", "[email protected]")));
}
}
Verification
Basic Verification
class VerificationExamplesTest {
@Mock
private EmailService emailService;
@Mock
private AuditService auditService;
@InjectMocks
private OrderService orderService;
@Test
void shouldVerifyMethodCalls() {
Order order = new Order();
order.setId(1L);
order.setCustomer(new Customer("John", "[email protected]"));
orderService.completeOrder(order);
// Verify method was called
verify(emailService).sendOrderConfirmation(order);
// Verify with specific arguments
verify(auditService).logEvent("ORDER_COMPLETED", order.getId());
// Verify method was never called
verify(emailService, never()).sendCancellationEmail(any());
// Verify number of interactions
verify(emailService, times(1)).sendOrderConfirmation(order);
verify(auditService, atLeastOnce()).logEvent(anyString(), anyLong());
verify(auditService, atMost(2)).logEvent(anyString(), anyLong());
// Verify no more interactions
verifyNoMoreInteractions(emailService);
}
@Test
void shouldVerifyCallOrder() {
Order order = new Order();
orderService.processOrder(order);
// Verify order of calls
InOrder inOrder = inOrder(auditService, emailService);
inOrder.verify(auditService).logEvent("ORDER_PROCESSING_STARTED", order.getId());
inOrder.verify(emailService).sendOrderConfirmation(order);
inOrder.verify(auditService).logEvent("ORDER_PROCESSING_COMPLETED", order.getId());
}
@Test
void shouldVerifyWithTimeout() {
Order order = new Order();
// Start async processing
orderService.processOrderAsync(order);
// Verify with timeout for async operations
verify(emailService, timeout(5000)).sendOrderConfirmation(order);
verify(auditService, timeout(5000).times(2))
.logEvent(anyString(), eq(order.getId()));
}
}
Argument Verification
class ArgumentVerificationTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Test
void shouldVerifyArguments() {
User user = new User("John", "[email protected]");
user.setAge(25);
userRepository.save(user);
emailService.sendWelcomeEmail(user.getEmail(), user.getFirstName());
// Verify with argument captor
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertThat(capturedUser.getFirstName()).isEqualTo("John");
assertThat(capturedUser.getAge()).isEqualTo(25);
// Verify with argument matchers
verify(emailService).sendWelcomeEmail(
eq("[email protected]"),
argThat(name -> name.startsWith("J"))
);
// Multiple argument captors
ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
verify(emailService).sendWelcomeEmail(
emailCaptor.capture(),
nameCaptor.capture()
);
assertThat(emailCaptor.getValue()).isEqualTo("[email protected]");
assertThat(nameCaptor.getValue()).isEqualTo("John");
}
}
Argument Matchers
Built-in Matchers
class ArgumentMatchersTest {
@Mock
private UserService userService;
@Test
void shouldUseBuiltInMatchers() {
// Exact match
when(userService.findById(1L)).thenReturn(new User("John", "[email protected]"));
// Any matchers
when(userService.updateUser(any(User.class))).thenReturn(true);
when(userService.findByEmail(anyString())).thenReturn(new User("Any", "[email protected]"));
when(userService.findByAge(anyInt())).thenReturn(Collections.emptyList());
// Null matchers
when(userService.findByEmail(isNull())).thenThrow(new IllegalArgumentException());
when(userService.findByEmail(isNotNull())).thenReturn(new User("NotNull", "[email protected]"));
// Collection matchers
when(userService.findByIds(anyList())).thenReturn(Collections.emptyList());
when(userService.updateUsers(anyCollection())).thenReturn(5);
// String matchers
when(userService.findByEmail(contains("@gmail.com")))
.thenReturn(new User("Gmail", "[email protected]"));
when(userService.findByFirstName(startsWith("J")))
.thenReturn(Collections.singletonList(new User("John", "[email protected]")));
when(userService.findByLastName(endsWith("son")))
.thenReturn(Collections.singletonList(new User("Johnson", "[email protected]")));
when(userService.findByFirstName(matches("[A-Z][a-z]+")))
.thenReturn(Collections.emptyList());
}
@Test
void shouldUseCustomMatchers() {
// Custom argument matcher
when(userService.updateUser(argThat(user ->
user.getAge() >= 18 && user.getEmail().contains("@")
))).thenReturn(true);
// Predicate matcher
when(userService.findByAge(intThat(age -> age >= 18 && age <= 65)))
.thenReturn(Collections.singletonList(new User("Adult", "[email protected]")));
// Verification with custom matchers
User user = new User("John", "[email protected]");
user.setAge(25);
userService.updateUser(user);
verify(userService).updateUser(argThat(u ->
u.getFirstName().equals("John") &&
u.getAge() > 18
));
}
}
Custom Matchers
class CustomMatchersTest {
// Custom matcher class
static class UserMatcher implements ArgumentMatcher<User> {
private final String expectedEmail;
private final Integer minAge;
public UserMatcher(String expectedEmail, Integer minAge) {
this.expectedEmail = expectedEmail;
this.minAge = minAge;
}
@Override
public boolean matches(User user) {
if (user == null) return false;
boolean emailMatches = expectedEmail == null ||
expectedEmail.equals(user.getEmail());
boolean ageMatches = minAge == null ||
(user.getAge() != null && user.getAge() >= minAge);
return emailMatches && ageMatches;
}
@Override
public String toString() {
return String.format("UserMatcher(email=%s, minAge=%s)", expectedEmail, minAge);
}
}
// Custom matcher factory methods
static User userWithEmail(String email) {
return argThat(new UserMatcher(email, null));
}
static User adultUser() {
return argThat(new UserMatcher(null, 18));
}
static User userWithEmailAndMinAge(String email, int minAge) {
return argThat(new UserMatcher(email, minAge));
}
@Mock
private UserService userService;
@Test
void shouldUseCustomMatchers() {
// Using custom matchers in stubbing
when(userService.updateUser(userWithEmail("[email protected]")))
.thenReturn(true);
when(userService.updateUser(adultUser()))
.thenReturn(true);
// Using custom matchers in verification
User user = new User("John", "[email protected]");
user.setAge(25);
userService.updateUser(user);
verify(userService).updateUser(userWithEmailAndMinAge("[email protected]", 18));
}
}
Advanced Mocking
Spying on Real Objects
class SpyingExamplesTest {
@Test
void shouldSpyOnRealObjects() {
// Spy on real object
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// Real method calls work
spyList.add("one");
spyList.add("two");
assertThat(spyList).hasSize(2);
// But we can stub specific methods
when(spyList.size()).thenReturn(100);
assertThat(spyList.size()).isEqualTo(100);
// Verify real method was called
verify(spyList).add("one");
verify(spyList, times(2)).add(anyString());
}
@Test
void shouldSpyOnService() {
UserService realUserService = new UserService(new UserRepository() {
public User findById(Long id) {
return new User("Real User", "[email protected]");
}
// ... other methods
});
UserService spyUserService = spy(realUserService);
// Stub specific method
doReturn(new User("Stubbed User", "[email protected]"))
.when(spyUserService).findById(999L);
// Real method for other IDs
User realUser = spyUserService.findById(1L);
assertThat(realUser.getFirstName()).isEqualTo("Real User");
// Stubbed method for ID 999
User stubbedUser = spyUserService.findById(999L);
assertThat(stubbedUser.getFirstName()).isEqualTo("Stubbed User");
// Verify method calls
verify(spyUserService).findById(1L);
verify(spyUserService).findById(999L);
}
}
Mocking Static Methods and Final Classes
class StaticMockingTest {
@Test
void shouldMockStaticMethods() {
try (MockedStatic<LocalDateTime> mockedDateTime = mockStatic(LocalDateTime.class)) {
LocalDateTime fixedTime = LocalDateTime.of(2024, 1, 1, 12, 0, 0);
mockedDateTime.when(LocalDateTime::now).thenReturn(fixedTime);
// Test code that uses LocalDateTime.now()
TimeService timeService = new TimeService();
String timestamp = timeService.getCurrentTimestamp();
assertThat(timestamp).contains("2024-01-01");
// Verify static method was called
mockedDateTime.verify(LocalDateTime::now);
}
}
@Test
void shouldMockStaticMethodsWithArguments() {
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
mockedFiles.when(() -> Files.readString(any(Path.class)))
.thenReturn("mocked file content");
FileService fileService = new FileService();
String content = fileService.readConfigFile("config.txt");
assertThat(content).isEqualTo("mocked file content");
mockedFiles.verify(() -> Files.exists(any(Path.class)));
mockedFiles.verify(() -> Files.readString(any(Path.class)));
}
}
@Test
void shouldMockConstructors() {
try (MockedConstruction<FileWriter> mockedConstruction =
mockConstruction(FileWriter.class)) {
FileService fileService = new FileService();
fileService.writeToFile("test.txt", "content");
// Verify constructor was called
assertThat(mockedConstruction.constructed()).hasSize(1);
FileWriter mockFileWriter = mockedConstruction.constructed().get(0);
verify(mockFileWriter).write("content");
verify(mockFileWriter).close();
}
}
}
Mocking Complex Scenarios
class ComplexMockingTest {
@Mock
private PaymentGateway paymentGateway;
@Mock
private EmailService emailService;
@Mock
private AuditService auditService;
@Test
void shouldMockComplexWorkflow() {
// Setup complex stubbing
when(paymentGateway.processPayment(any()))
.thenReturn(CompletableFuture.completedFuture(
new PaymentResult("txn-123", PaymentStatus.SUCCESS, BigDecimal.valueOf(100))
));
when(emailService.sendEmail(anyString(), anyString(), anyString()))
.thenReturn(CompletableFuture.completedFuture(true));
doAnswer(invocation -> {
String event = invocation.getArgument(0);
Object data = invocation.getArgument(1);
System.out.println("Audit: " + event + " - " + data);
return null;
}).when(auditService).logEvent(anyString(), any());
// Test the workflow
OrderService orderService = new OrderService(paymentGateway, emailService, auditService);
Order order = new Order();
order.setTotalAmount(BigDecimal.valueOf(100));
order.setCustomer(new Customer("John", "[email protected]"));
CompletableFuture<Boolean> result = orderService.processOrderAsync(order);
assertThat(result.join()).isTrue();
// Verify the workflow
verify(paymentGateway).processPayment(any());
verify(emailService).sendEmail(eq("[email protected]"), anyString(), anyString());
verify(auditService, times(2)).logEvent(anyString(), any());
}
@Test
void shouldMockExceptionScenarios() {
// Simulate payment failure
when(paymentGateway.processPayment(any()))
.thenReturn(CompletableFuture.failedFuture(
new PaymentException("Insufficient funds")
));
// Email should not be sent on payment failure
OrderService orderService = new OrderService(paymentGateway, emailService, auditService);
Order order = new Order();
assertThrows(PaymentException.class, () -> {
orderService.processOrder(order);
});
verify(paymentGateway).processPayment(any());
verify(emailService, never()).sendEmail(anyString(), anyString(), anyString());
verify(auditService).logEvent(eq("PAYMENT_FAILED"), any());
}
}
Best Practices
Effective Mock Usage
class MockingBestPracticesTest {
// Good: Mock external dependencies
@Mock
private EmailService emailService; // External service
@Mock
private PaymentGateway paymentGateway; // External API
@Mock
private UserRepository userRepository; // Data access layer
// Good: Don't mock the class under test
@InjectMocks
private OrderService orderService; // System under test - not mocked
@Test
void shouldFollowMockingBestPractices() {
// Good: Stub only what you need
when(userRepository.findById(1L))
.thenReturn(new User("John", "[email protected]"));
// Good: Use specific argument matchers
when(emailService.sendEmail(
eq("[email protected]"),
eq("Order Confirmation"),
contains("Your order")
)).thenReturn(true);
// Test business logic
Order order = orderService.createOrder(1L, "product-123", 2);
// Good: Verify important interactions
verify(userRepository).findById(1L);
verify(emailService).sendEmail(anyString(), anyString(), anyString());
// Good: Assert on the result, not just interactions
assertThat(order).isNotNull();
assertThat(order.getCustomer().getFirstName()).isEqualTo("John");
}
@Test
void shouldAvoidOverMocking() {
// Bad: Don't mock value objects
// Date mockDate = mock(Date.class); // DON'T DO THIS
// Good: Use real value objects
Date realDate = new Date();
// Bad: Don't mock everything
// String mockString = mock(String.class); // DON'T DO THIS
// Good: Use real simple objects
String realString = "test value";
// Test with real objects where appropriate
User user = new User("John", "[email protected]"); // Real object
when(userRepository.save(user)).thenReturn(user);
User result = userRepository.save(user);
assertThat(result).isEqualTo(user);
}
@Test
void shouldUseDescriptiveNames() {
// Good: Use descriptive test names
// shouldCreateOrderWhenUserExistsAndPaymentSucceeds()
// shouldThrowExceptionWhenUserNotFound()
// shouldSendConfirmationEmailAfterSuccessfulOrder()
// Good: Use descriptive variable names
User existingCustomer = new User("John", "[email protected]");
PaymentResult successfulPayment = new PaymentResult("txn-123", PaymentStatus.SUCCESS);
when(userRepository.findById(1L)).thenReturn(existingCustomer);
when(paymentGateway.processPayment(any())).thenReturn(successfulPayment);
// Test implementation
Order order = orderService.createOrder(1L, "product-123", 1);
assertThat(order.getCustomer()).isEqualTo(existingCustomer);
}
@Test
void shouldResetMocksBetweenTests() {
// Mocks are automatically reset between tests with @Mock annotation
// But if you need to reset manually:
when(userRepository.findById(1L)).thenReturn(new User("John", "[email protected]"));
// Use the mock
User user = userRepository.findById(1L);
assertThat(user.getFirstName()).isEqualTo("John");
// Reset if needed (usually not necessary)
reset(userRepository);
// Mock behavior is now reset
User nullUser = userRepository.findById(1L);
assertThat(nullUser).isNull(); // Default mock behavior
}
}
Testing Anti-Patterns to Avoid
class MockingAntiPatternsTest {
@Mock
private UserRepository userRepository;
@Test
void shouldAvoidCommonAntiPatterns() {
// ANTI-PATTERN 1: Mocking concrete classes when interface exists
// UserService mockUserService = mock(UserServiceImpl.class); // DON'T
UserService mockUserService = mock(UserService.class); // DO
// ANTI-PATTERN 2: Over-stubbing (stubbing unused methods)
when(userRepository.findById(any())).thenReturn(new User("John", "[email protected]"));
when(userRepository.findByEmail(any())).thenReturn(new User("Jane", "[email protected]")); // Unused
when(userRepository.count()).thenReturn(100L); // Unused
// Only test actually uses findById, other stubs are unnecessary
User user = userRepository.findById(1L);
assertThat(user).isNotNull();
// ANTI-PATTERN 3: Testing implementation details instead of behavior
// verify(userRepository).findById(1L); // Tests how, not what
// BETTER: Test the actual behavior/outcome
assertThat(user.getFirstName()).isEqualTo("John");
// ANTI-PATTERN 4: Mocking types you don't own without good reason
// List<String> mockList = mock(List.class); // Usually unnecessary
List<String> realList = new ArrayList<>(); // Use real implementation
// ANTI-PATTERN 5: Complex stubbing that obscures the test intent
when(userRepository.findById(any())).thenAnswer(invocation -> {
Long id = invocation.getArgument(0);
if (id == 1L) {
User user1 = new User("John", "[email protected]");
user1.setActive(true);
user1.setCreatedAt(LocalDateTime.now().minusDays(30));
return user1;
} else if (id == 2L) {
User user2 = new User("Jane", "[email protected]");
user2.setActive(false);
user2.setCreatedAt(LocalDateTime.now().minusDays(60));
return user2;
}
return null;
});
// BETTER: Use simple, focused stubs or test data builders
User simpleUser = new User("John", "[email protected]");
when(userRepository.findById(1L)).thenReturn(simpleUser);
}
}
Summary
Mocking is essential for effective unit testing in Java:
Key Benefits:
- Isolation: Test units without dependencies
- Control: Define exact behavior of collaborators
- Speed: Eliminate slow external operations
- Reliability: Remove external failure points
Mockito Features:
- Easy Mock Creation: Annotations and programmatic creation
- Flexible Stubbing: Return values, exceptions, custom behavior
- Comprehensive Verification: Method calls, arguments, order
- Argument Matching: Built-in and custom matchers
- Advanced Features: Spies, static mocking, constructor mocking
Best Practices:
- Mock external dependencies, not the system under test
- Use descriptive test and variable names
- Stub only what you need for the test
- Verify important interactions, but focus on behavior
- Avoid over-mocking and testing implementation details
- Use real objects for simple value types
Common Pitfalls:
- Mocking types you don't own unnecessarily
- Over-stubbing unused behavior
- Testing implementation rather than behavior
- Complex stubbing that obscures test intent
Effective mocking with Mockito enables writing fast, reliable, and maintainable unit tests that provide confidence in your Java applications.