jUnit Stuff
Mockito Unit Test vs Spring Boot Test
This article organizes two common testing methods:
- Pure Unit Testing (Mockito + JUnit)
- Spring Boot Testing (@SpringBootTest excluding auto-configuration)
Mockito JUnit
Features:
- Does not depend on Spring container
- Uses
@ExtendWith(MockitoExtension.class) - Suitable for Service layer logic testing
Example Code
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class CodeEntityServiceImplTest {
@InjectMocks
private CodeEntityServiceImpl codeEntityService;
@Mock
private CodeEntityMapper codeEntityMapper;
@Test
void testGetByIdMine_success() {
CodeEntityPO po = new CodeEntityPO();
po.setUsername("Candy");
po.setCode("abc");
Mockito.when(codeEntityMapper.selectById("abc")).thenReturn(po);
CodeEntityPO result = codeEntityService.getByIdMine("abc");
assertEquals("Candy", result.getUsername());
assertEquals("abc", result.getCode());
}
@Test
void testGetByIdMine_nullShouldThrow() {
Mockito.when(codeEntityMapper.selectById("abc")).thenReturn(null);
Exception ex = assertThrows(IllegalArgumentException.class, () -> {
codeEntityService.getByIdMine1("abc");
});
assertEquals("Object must not be null", ex.getMessage());
}
}
✅ Pros:
- Fast execution
- Does not depend on database or Spring container
- Logic testing is clear
Spring Boot test
Features:
- Depends on Spring Boot context, but excludes database etc. auto-configuration
- Uses
@MockBeanto replace Mapper or Service - Suitable for Service/Controller logic testing
Example Code
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootTest(
classes = YourMainApplication.class,
properties = {
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"
}
)
class CodeEntityServiceImplBootTest {
@Autowired
private CodeEntityServiceImpl codeEntityService;
@MockBean
private CodeEntityMapper codeEntityMapper;
@Test
void testGetByIdMine_success() {
CodeEntityPO po = new CodeEntityPO();
po.setUsername("Candy");
po.setCode("abc");
Mockito.when(codeEntityMapper.selectById("abc")).thenReturn(po);
CodeEntityPO result = codeEntityService.getByIdMine("abc");
assertEquals("Candy", result.getUsername());
assertEquals("abc", result.getCode());
}
}
Can also use yml file to exclude auto-configuration:
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
@SpringBootTest(classes = App.class)
@ActiveProfiles("test")
✅ Pros:
- Spring context available, dependency injection normal
- Can test Service with transactions or AOP
- Avoid database connection errors
Static Method Mock
Static Method Mock
import com.whalefall541.staticmock.ExternalLib;
import com.whalefall541.staticmock.MyService;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
class StaticMockTest {
@Test
void testStaticMockWithSupplier() {
// 1. Create the static mock within a try-with-resources block
try (MockedStatic<ExternalLib> mockedLib = mockStatic(ExternalLib.class)) {
// 2. Define behavior: When compute() is called with ANY Supplier, return "Mocked"
mockedLib.when(() -> ExternalLib.compute(any(Supplier.class)))
.thenReturn("Mocked Result");
// 3. Execute the service method
MyService service = new MyService();
String result = service.getProcessedData();
// 4. Verify the result is what we mocked
assertEquals("Mocked Result", result);
// Optional: Verify the static method was called exactly once
mockedLib.verify(() -> ExternalLib.compute(any(Supplier.class)));
}
}
@Test
void testGetProcessedData1_FailureInsideSupplier() {
try (MockedStatic<ExternalLib> mockedLib = mockStatic(ExternalLib.class)) {
mockedLib.when(() -> ExternalLib.compute(any()))
.thenCallRealMethod();
MyService service = new MyService();
Exception exception = assertThrows(IllegalArgumentException.class, service::getProcessedData1);
assertEquals("This will fail", exception.getMessage());
}
}
}
Assert log content
-
LogCaptor (Concurrent execution scenario has problems)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(OutputCaptureExtension.class)
// If CapturedOutput is used by several methods, just opening next line comment
// @SpringBootTest
class MyServiceTest {
@Test
void shouldLogMessage(CapturedOutput output) {
MyService service = new MyService();
service.doSomething();
assertThat(output.getOut()).contains("Expected log message");
}
}
Mock annotation
- using spring boot test
getting the real annotation from classes
- using a tempt inner class
@UseTemplate(DemoTemplate.class)
static class DemoBusiness implements Business<String> {
@Override
public void process(String input) {
// no-op
}
}
static class DemoTemplate implements Template<String, DemoBusiness> {
@Override
public void handler(String txcode, String param, DemoBusiness businessService) {
// mock behavior
}
}
@Test
void testInit_withUseTemplateAnnotation() {
ApplicationContext ctx = mock(ApplicationContext.class);
DemoTemplate demoTemplate = new DemoTemplate();
when(ctx.getBean(DemoTemplate.class)).thenReturn(demoTemplate);
Map<String, DemoBusiness> map = Map.of("job1", new DemoBusiness());
RegisterEnginV3<String, DemoBusiness> engin =
new RegisterEnginV3<>(map, Map.of(), new HashMap<>(), ctx);
engin.init(); // ✅ Will read @UseTemplate(DemoTemplate.class)
}
- new a annotion instance to use.
Influence between unit test methods in the same class
very call count is originally once, previous test method will affect subsequent methods, need to add this on unit test class.
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
Depends on @PostConstruct
You can create it, pass parameter by new construction, then call the init method .
@Service
@AllArgsConstructor
public class RegisterEnginV3<T, S extends Business<T>> {
@PostConstruct
public void init() {
businessesMap.forEach((txcode, businessService) -> {
// 1. Resolve @UseTemplate annotation
UseTemplate ann = businessService.getClass()
.getAnnotation(UseTemplate.class);
// 2. Get corresponding Template Bean
Template<T, S> templateToUse =
(Template<T, S>) applicationContext
.getBean(ann.value());
// 3. Generic type check
checkConsistentGenericType(businessService, templateToUse);
// 4. Register to execution registry
registry.put(txcode,
param -> templateToUse.handler(txcode, param, businessService));
});
}
public void run(String businessType, T params) {
registry.get(businessType).accept(params);
}
}
@MockBean Failure Problem
debug found that a property inside a @Resource object was already written with @MockBean in unit test but still injected real object
✅ Solution 1: Use instead
@Autowired
Most direct, officially recommended way.
@MockBean is designed specifically for @Autowired mechanism.
Mocked method changes argument properties
Very good question 👏
Your scenario is very typical:
✅ Problem Scenario
An object mocked by @MockBean (such as service or client),
It has a void method, which modifies the object you passed in (e.g. setting fields).
In unit test, you want to mock this behavior, letting the object's properties be set.
@Service
public class OrderService {
@Autowired
private PaymentClient paymentClient;
public void process(OrderContext ctx) {
paymentClient.fillPaymentInfo(ctx); // void method, internally sets ctx fields
}
}
public class PaymentClient {
public void fillPaymentInfo(OrderContext ctx) {
ctx.setPayStatus("SUCCESS");
ctx.setPayAmount(100);
}
}
doAnswer() allows you to define custom behavior when calling void methods.
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean
private PaymentClient paymentClient;
@Test
void testProcess() {
// 1️⃣ Mock PaymentClient behavior for fillPaymentInfo
doAnswer(invocation -> {
OrderContext ctx = invocation.getArgument(0);
ctx.setPayStatus("MOCK_SUCCESS");
ctx.setPayAmount(999);
return null; // Because target method is void
}).when(paymentClient).fillPaymentInfo(any(OrderContext.class));
// 2️⃣ Call actual business
OrderContext context = new OrderContext();
orderService.process(context);
// 3️⃣ Assert mock effect
assertEquals("MOCK_SUCCESS", context.getPayStatus());
assertEquals(999, context.getPayAmount());
}
}
- Attribution: Retain the original author's signature and code source information in the original and derivative code.
- Preserve License: Retain the Apache 2.0 license file in the original and derivative code.
- Attribution: Give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial: You may not use the material for commercial purposes. For commercial use, please contact the author.
- ShareAlike: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.