1. java
  2. /testing
  3. /mocking

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

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.