Mockito Unit Testing: A Complete Guide to Testing Java Service Layers
This article explains unit testing fundamentals, compares unit and integration tests, introduces Mockito's core annotations and APIs, walks through a full order‑resend example with detailed test cases, and covers best‑practice methodologies, advanced techniques, common pitfalls, naming conventions, and how to run the tests.
What is Unit Testing
Unit testing validates the smallest testable unit—usually a single method—by verifying its behavior under various inputs, quickly catching logic errors and providing a safety net for refactoring.
Validate method behavior under different conditions
Detect code logic errors early
Provide a safety net for refactoring
Unit Test vs Integration Test
Scope : single method/class vs multiple components
Dependencies : all mocked vs real (DB, MQ, etc.)
Speed : milliseconds vs seconds or minutes
Environment : no Spring container vs full environment
Use case : verify business logic branches vs verify component interaction
Mockito Core Concepts
Mockito is the most popular Java mock framework. Its core idea is to replace real dependencies with virtual objects so that only the logic of the method under test is exercised.
Key Annotations
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class OrderServiceImplTest {
@InjectMocks
private OrderServiceImpl orderService;
@Mock
private OrderRepository orderRepository;
@Mock
private UserRepository userRepository;
@Mock
private MessageSender messageSender;
}Initialization
// Option 1: manual init in @BeforeEach (recommended, good compatibility)
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
// Option 2: JUnit5 extension (more concise)
@ExtendWith(MockitoExtension.class)
public class OrderServiceImplTest { /* ... */ }Mockito Core API
when(...).thenReturn(...)
// Basic usage: when findById(1) is called, return an Order
Order mockOrder = new Order();
mockOrder.setId(1);
mockOrder.setStatus("CREATED");
when(orderRepository.findById(1)).thenReturn(Optional.of(mockOrder));
// Return empty
when(orderRepository.findById(999)).thenReturn(Optional.empty());
// Return a list
List<Order> orderList = Arrays.asList(mockOrder);
when(orderRepository.findByUserId(100)).thenReturn(orderList);Argument Matchers
import static org.mockito.ArgumentMatchers.*;
when(orderRepository.save(any(Order.class))).thenReturn(mockOrder);
when(orderRepository.findById(anyInt())).thenReturn(Optional.of(mockOrder));
when(userRepository.findByName(anyString())).thenReturn(mockUser);
when(orderRepository.findByUserIdAndStatus(eq(100), anyString()))
.thenReturn(orderList);
when(orderRepository.save(argThat(order -> order.getStatus().equals("PAID"))))
.thenReturn(mockOrder);Important rule : if any argument uses a matcher, all arguments must use matchers.
// Wrong: mixing concrete value and matcher
when(orderRepository.findByUserIdAndStatus(100, anyString()))
.thenReturn(orderList);
// Correct: use matchers for all arguments
when(orderRepository.findByUserIdAndStatus(eq(100), anyString()))
.thenReturn(orderList);when(...).thenThrow(...)
// Simulate exception from repository
when(orderRepository.findById(anyInt()))
.thenThrow(new RuntimeException("Database connection failed"));
// Simulate exception from void method
doThrow(new RuntimeException("Send failed")).when(messageSender).send(any());verify(...)
// Verify save called once
verify(orderRepository).save(any(Order.class));
// Verify save called twice
verify(orderRepository, times(2)).save(any(Order.class));
// Verify method never called
verify(messageSender, never()).send(any());
// Verify specific argument content
verify(orderRepository).save(argThat(order -> {
return order.getStatus().equals("CANCELLED") &&
order.getUserId().equals(100);
}));doReturn / doNothing / doThrow
// Void method does nothing
doNothing().when(messageSender).send(any());
// Void method throws
doThrow(new RuntimeException("error")).when(messageSender).send(any());
// Consecutive calls return different values
when(orderRepository.findById(1))
.thenReturn(Optional.of(order1)) // first call
.thenReturn(Optional.of(order2)); // second callFull Example: Order Resend Scenario
Business Code
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderRepository orderRepository;
@Resource
private OrderLogRepository orderLogRepository;
@Resource
private NotificationSender notificationSender;
/**
* Process order resend logic.
* If a tracking number already has a push record and the old order is cancelled:
* 1. Mark old record as "invalid"
* 2. Create a new push record
* 3. Trigger notification
*/
public Integer processResend(String trackingNo, Integer newOrderId) {
OrderLog existingLog = orderLogRepository.findByTrackingNoAndType(trackingNo, 6);
if (existingLog == null) {
Order newOrder = orderRepository.findById(newOrderId).orElse(null);
if (newOrder == null) return null;
OrderLog newLog = new OrderLog();
newLog.setOrderId(newOrderId);
newLog.setTrackingNo(trackingNo);
newLog.setType(6);
newLog.setStatus(0); // not sent
orderLogRepository.save(newLog);
return newLog.getId();
} else {
Order oldOrder = orderRepository.findById(existingLog.getOrderId()).orElse(null);
if (oldOrder != null && "CANCELLED".equals(oldOrder.getStatus())) {
existingLog.setStatus(4); // invalidated
orderLogRepository.save(existingLog);
Order newOrder = orderRepository.findById(newOrderId).orElse(null);
OrderLog newLog = new OrderLog();
newLog.setOrderId(newOrderId);
newLog.setTrackingNo(trackingNo);
newLog.setType(6);
newLog.setStatus(0);
orderLogRepository.save(newLog);
return newLog.getId();
}
if (existingLog.getStatus() == 1 || existingLog.getStatus() == 3) {
return null; // already success or in delay, no duplicate push
}
return existingLog.getId();
}
}
}Complete Unit Tests
package com.example.service.impl;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import com.example.entity.Order;
import com.example.entity.OrderLog;
import com.example.repository.OrderLogRepository;
import com.example.repository.OrderRepository;
import com.example.sender.NotificationSender;
import java.util.Optional;
import org.junit.jupiter.api.*;
import org.mockito.*;
class OrderServiceImplTest {
@InjectMocks
private OrderServiceImpl orderService;
@Mock
private OrderRepository orderRepository;
@Mock
private OrderLogRepository orderLogRepository;
@Mock
private NotificationSender notificationSender;
private AutoCloseable mockitoCloseable;
@BeforeEach
void setUp() {
mockitoCloseable = MockitoAnnotations.openMocks(this);
}
@AfterEach
void tearDown() throws Exception {
mockitoCloseable.close();
}
// ===== Scenario 1: First push (no history) =====
@Test
@DisplayName("First push – no history creates new record and returns ID")
void processResend_noExistingLog_shouldCreateNewLog() {
String trackingNo = "SF123456";
Integer newOrderId = 100;
when(orderLogRepository.findByTrackingNoAndType(trackingNo, 6)).thenReturn(null);
Order newOrder = new Order();
newOrder.setId(newOrderId);
newOrder.setStatus("SHIPPING");
when(orderRepository.findById(newOrderId)).thenReturn(Optional.of(newOrder));
when(orderLogRepository.save(any(OrderLog.class))).thenAnswer(invocation -> {
OrderLog saved = invocation.getArgument(0);
saved.setId(1);
return saved;
});
Integer result = orderService.processResend(trackingNo, newOrderId);
assertNotNull(result);
assertEquals(1, result);
ArgumentCaptor<OrderLog> captor = ArgumentCaptor.forClass(OrderLog.class);
verify(orderLogRepository, times(1)).save(captor.capture());
OrderLog savedLog = captor.getValue();
assertEquals(newOrderId, savedLog.getOrderId());
assertEquals(trackingNo, savedLog.getTrackingNo());
assertEquals(6, savedLog.getType());
assertEquals(0, savedLog.getStatus());
}
// ===== Scenario 2: Cancelled resend (core case) =====
@Test
@DisplayName("Cancelled resend – old order cancelled should invalidate old and create new")
void processResend_oldOrderCancelled_shouldInvalidateOldAndCreateNew() {
String trackingNo = "SF123456";
Integer oldOrderId = 50;
Integer newOrderId = 100;
OrderLog existingLog = new OrderLog();
existingLog.setId(10);
existingLog.setOrderId(oldOrderId);
existingLog.setTrackingNo(trackingNo);
existingLog.setType(6);
existingLog.setStatus(1); // success
when(orderLogRepository.findByTrackingNoAndType(trackingNo, 6)).thenReturn(existingLog);
Order oldOrder = new Order();
oldOrder.setId(oldOrderId);
oldOrder.setStatus("CANCELLED");
when(orderRepository.findById(oldOrderId)).thenReturn(Optional.of(oldOrder));
Order newOrder = new Order();
newOrder.setId(newOrderId);
newOrder.setStatus("SHIPPING");
when(orderRepository.findById(newOrderId)).thenReturn(Optional.of(newOrder));
when(orderLogRepository.save(any(OrderLog.class))).thenAnswer(invocation -> {
OrderLog saved = invocation.getArgument(0);
if (saved.getId() == null) saved.setId(20);
return saved;
});
Integer result = orderService.processResend(trackingNo, newOrderId);
assertEquals(20, result);
ArgumentCaptor<OrderLog> captor = ArgumentCaptor.forClass(OrderLog.class);
verify(orderLogRepository, times(2)).save(captor.capture());
OrderLog firstSave = captor.getAllValues().get(0);
assertEquals(10, firstSave.getId());
assertEquals(4, firstSave.getStatus()); // invalidated
OrderLog secondSave = captor.getAllValues().get(1);
assertEquals(newOrderId, secondSave.getOrderId());
assertEquals(trackingNo, secondSave.getTrackingNo());
assertEquals(6, secondSave.getType());
assertEquals(0, secondSave.getStatus());
}
// Additional scenarios (old not cancelled, order not found, failure retry) follow the same pattern.
}Test Writing Methodology: Given‑When‑Then
@Test
void testMethodName_condition_expectedBehavior() {
// ===== Given (prepare) =====
// 1. Build input parameters
// 2. Stub mock returns
// 3. Define when(...).thenReturn(...)
// ===== When (execute) =====
// Call the method under test
// ===== Then (verify) =====
// 1. assertEquals / assertNull / assertNotNull for return value
// 2. verify(...) for dependency calls
// 3. ArgumentCaptor to capture and assert passed arguments
}Advanced Techniques
ArgumentCaptor
ArgumentCaptor<OrderLog> captor = ArgumentCaptor.forClass(OrderLog.class);
verify(orderLogRepository).save(captor.capture());
OrderLog captured = captor.getValue();
assertEquals("SF123456", captured.getTrackingNo());
assertEquals(0, captured.getStatus());thenAnswer for Dynamic Returns
when(orderLogRepository.save(any(OrderLog.class)))
.thenAnswer(invocation -> {
OrderLog saved = invocation.getArgument(0);
saved.setId(100); // simulate DB generated ID
return saved;
});Verifying Call Order
InOrder inOrder = inOrder(orderLogRepository);
// Verify old record is invalidated first
inOrder.verify(orderLogRepository).save(argThat(log -> log.getStatus() == 4));
// Then new record is created
inOrder.verify(orderLogRepository).save(argThat(log -> log.getStatus() == 0));ReflectionTestUtils for @Value fields
ReflectionTestUtils.setField(orderService, "maxRetryCount", 3);
ReflectionTestUtils.setField(orderService, "warehouseId", "WH001");Testing Private Methods
Method method = OrderServiceImpl.class.getDeclaredMethod("processResend", String.class, Integer.class);
method.setAccessible(true);
Integer result = (Integer) method.invoke(orderService, "SF123456", 100);
assertEquals(1, result);Common Issues and Pitfalls
NullPointerException
Cause: a mocked method was not stubbed, so it returns null and subsequent calls on the result trigger NPE.
// Forgot to stub – NPE occurs
Order order = orderRepository.findById(1).orElse(null);
order.getStatus(); // NPE
// Correct: stub first
when(orderRepository.findById(1)).thenReturn(Optional.of(mockOrder));Unnecessary Stubbing Exception
Cause: a stubbed method is never invoked.
// Solution 1: remove the unused stub
// Solution 2: use lenient mode
lenient().when(orderRepository.findById(anyInt())).thenReturn(Optional.empty());Mock Returning Null Instead of Expected Value
Cause: argument mismatch.
// when(repo.findById(1)) works only for exact 1
// Use any() when the exact value is not important
when(orderRepository.findById(anyInt())).thenReturn(Optional.of(mockOrder));Void Methods Cannot Use when().thenReturn()
// Compilation error
// when(messageSender.send(any())).thenReturn(null);
// Correct: use doNothing or doThrow
doNothing().when(messageSender).send(any());
doThrow(new RuntimeException("error")).when(messageSender).send(any());Test Naming Conventions
Recommended format:
methodName_condition_expectedBehavior // Good names
void processResend_oldOrderCancelled_shouldCreateNewLog();
void processResend_noExistingLog_shouldReturnNewId();
void processResend_statusAlreadySuccess_shouldReturnNull();
// Bad names
void test1();
void testProcessResend();
void testSuccess();Running Tests
# Run all tests
mvn test
# Run a specific test class
mvn test -Dtest=OrderServiceImplTest
# Run a specific test method
mvn test -Dtest="OrderServiceImplTest#processResend_oldOrderCancelled_shouldInvalidateOldAndCreateNew"
# Skip test compilation and run directly
mvn test -pl . -Dtest=OrderServiceImplTestChecklist Before Writing a Unit Test
Identify the method under test
Map out branch paths – each if/else becomes a test case
Determine dependencies – mock all external calls (repositories, feign, MQ, etc.)
Construct data – prepare input parameters and mock return values for each branch
Verify results
Is the return value correct? (assertEquals)
Are dependent methods called as expected? (verify)
Are the passed arguments correct? (ArgumentCaptor)
Are unintended methods not called? (verify never)
Following this checklist lets you achieve comprehensive unit‑test coverage for any service method.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
The Dominant Programmer
Resources and tutorials for programmers' advanced learning journey. Advanced tracks in Java, Python, and C#. Blog: https://blog.csdn.net/badao_liumang_qizhi
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
