Skip to content

Mocking & Test Doubles


Definition

Test Doubles


Mockito Basics

// CREATING MOCKS

// Annotation-based (preferred)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks  // Injects mocks into constructor
    private OrderService orderService;

    // Tests...
}

// Programmatic
OrderRepository mockRepo = mock(OrderRepository.class);
PaymentService mockPayment = mock(PaymentService.class);
OrderService service = new OrderService(mockRepo, mockPayment);

// STUBBING (define behavior)

// Return value
when(repository.findById("123")).thenReturn(Optional.of(order));

// Throw exception
when(paymentService.charge(any())).thenThrow(new PaymentException());

// Multiple calls return different values
when(repository.findAll())
    .thenReturn(List.of(order1))
    .thenReturn(List.of(order1, order2));

// Answer based on input
when(repository.save(any(Order.class))).thenAnswer(invocation -> {
    Order order = invocation.getArgument(0);
    order.setId(UUID.randomUUID().toString());
    return order;
});

// Void methods
doThrow(new RuntimeException()).when(emailService).send(any());
doNothing().when(logger).log(any());

Verification

// VERIFYING INTERACTIONS

// Was method called?
verify(repository).save(order);

// Called with specific arguments
verify(emailService).sendEmail(eq("[email protected]"), anyString());

// Called exact number of times
verify(repository, times(2)).findById(any());
verify(repository, never()).delete(any());
verify(repository, atLeast(1)).save(any());
verify(repository, atMost(3)).findAll();

// Called with argument matching
verify(repository).save(argThat(order ->
    order.getStatus() == OrderStatus.PENDING
));

// Capture arguments for detailed assertions
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repository).save(captor.capture());
Order savedOrder = captor.getValue();
assertThat(savedOrder.getTotal()).isEqualTo(Money.of(100));

// Verify call order
InOrder inOrder = inOrder(paymentService, repository);
inOrder.verify(paymentService).charge(any());
inOrder.verify(repository).save(any());

// No more interactions
verifyNoMoreInteractions(repository);

// Zero interactions
verifyNoInteractions(emailService);

Spies and Partial Mocking

// SPY: Real object with partial mocking

// Create spy
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);

// Real methods are called
spyList.add("one");  // Actually adds to list
assertThat(spyList.size()).isEqualTo(1);

// But can stub specific methods
doReturn(100).when(spyList).size();
assertThat(spyList.size()).isEqualTo(100);

// ANNOTATION-BASED SPY
@Spy
private OrderValidator orderValidator = new OrderValidator();

@Test
void testWithSpy() {
    // Real validation logic runs
    // But can stub specific methods
    doReturn(true).when(orderValidator).checkInventory(any());
}

// USE CASES FOR SPIES:
// • Testing legacy code that can't be easily mocked
// • When most behavior should be real
// • Verifying that real methods were called

// CAUTION:
// Prefer mocks over spies in most cases
// Spies can be a sign of code smell (tight coupling)

// PARTIAL MOCKING (alternative)
@Mock
private OrderService orderService;

@Test
void testPartialMock() {
    // Stub one method
    when(orderService.calculateTotal(any())).thenReturn(Money.of(100));

    // Call real method for others
    when(orderService.validate(any())).thenCallRealMethod();
}

When to Mock

When to Mock


Fakes

// FAKE: Working but simplified implementation

// Interface
interface UserRepository {
    User save(User user);
    Optional<User> findById(String id);
    void delete(String id);
}

// Fake implementation
class FakeUserRepository implements UserRepository {
    private final Map<String, User> users = new HashMap<>();

    @Override
    public User save(User user) {
        if (user.getId() == null) {
            user.setId(UUID.randomUUID().toString());
        }
        users.put(user.getId(), user);
        return user;
    }

    @Override
    public Optional<User> findById(String id) {
        return Optional.ofNullable(users.get(id));
    }

    @Override
    public void delete(String id) {
        users.remove(id);
    }
}

// Usage in tests
class UserServiceTest {
    private final UserRepository repository = new FakeUserRepository();
    private final UserService service = new UserService(repository);

    @Test
    void shouldCreateUser() {
        User user = service.create("John", "[email protected]");

        assertThat(user.getId()).isNotNull();
        assertThat(repository.findById(user.getId())).isPresent();
    }
}

// WHEN TO USE FAKES:
// • Complex state that's hard to stub
// • Need realistic behavior across multiple calls
// • Integration-like tests without real infrastructure
// • Shared test fixtures across many tests

Tips & Tricks

Mocking Tips