Spring Boot服务层JUnit测试:Mocked Bean未执行问题排查
我正在使用Spring Boot REST API,想要测试服务层。我的服务类通过@Autowired字段注入MyRepository和ResponseMapper,没有用构造器注入(我偏好这种方式简化代码)。在JUnit测试中,我用MockitoJUnitRunner,通过@InjectMocks注入MyService,用@Mock注解了MyRepository和ResponseMapper。因为希望ResponseMapper的mapToResponseData方法实际执行而非被Mock,我尝试用ReflectionTestUtils.setField给MyService注入responseMapper,但调试发现这个方法根本没执行,导致断言失败。我知道可以Mock这个方法,但希望直接执行它来避免单独为Mapper写测试类。请问我哪里操作错了?这种注入Bean的方式是否正确?服务类必须用构造器注入Bean吗?
服务类代码
@Component public class MyService { @Autowired private MyRepository myRepository; @Autowired private ResponseMapper responseMapper; public List<MyObj> getList(String param) throws MyException { log.info("Getting details"); Optional<List<MyObj>> list = myRepository.findByParam(param); List<MyObj> data = responseMapper.mapToResponseData(list); return data; } }
测试类代码
@RunWith(MockitoJUnitRunner.class) public class MyServiceTest { @InjectMocks private MyService myService; @Mock private MyRepository myRepository; @Mock private ResponseMapper responseMapper; @Before public void setUp() { ReflectionTestUtils.setField(myService, "responseMapper", responseMapper); } @Test public void getListTest() throws Exception { when(myRepository.findByParam(anyString())) .thenReturn(Optional.of(getSampleList())); List<MyObj> list = myService.getList("param"); assertTrue(list.size() >0); } }
1. 你的操作错误在哪里?
核心问题出在ResponseMapper的实例上:你用@Mock注解了它,Mockito已经为它创建了一个Mock对象,之后用ReflectionTestUtils.setField注入的还是这个Mock对象,而非真实实例。Mock对象的所有方法默认返回空值(比如List返回空列表),所以mapToResponseData不会执行真实逻辑,自然导致断言失败。
要让ResponseMapper执行真实方法,你需要:
- 去掉
@Mock注解,直接创建它的真实实例(比如new ResponseMapper(),前提是它没有其他依赖); - 如果
ResponseMapper本身有Spring依赖,可以改用Spring Boot测试框架(@SpringBootTest+@MockBean)来管理Bean,但测试会相对重一些。
修改后的测试类示例:
@RunWith(MockitoJUnitRunner.class) public class MyServiceTest { @InjectMocks private MyService myService; @Mock private MyRepository myRepository; // 直接创建真实实例,而非Mock private ResponseMapper responseMapper = new ResponseMapper(); @Before public void setUp() { ReflectionTestUtils.setField(myService, "responseMapper", responseMapper); } @Test public void getListTest() throws Exception { when(myRepository.findByParam(anyString())) .thenReturn(Optional.of(getSampleList())); List<MyObj> list = myService.getList("param"); assertTrue(list.size() > 0); } // 补充测试数据方法 private List<MyObj> getSampleList() { return Arrays.asList(new MyObj(), new MyObj()); } }
另外,其实@InjectMocks会自动注入字段,如果你已经声明了真实的responseMapper,可以不用ReflectionTestUtils,直接让Mockito自动注入:
@RunWith(MockitoJUnitRunner.class) public class MyServiceTest { @InjectMocks private MyService myService; @Mock private MyRepository myRepository; // 直接声明真实实例,@InjectMocks会自动注入到MyService中 private ResponseMapper responseMapper = new ResponseMapper(); @Test public void getListTest() throws Exception { when(myRepository.findByParam(anyString())) .thenReturn(Optional.of(getSampleList())); List<MyObj> list = myService.getList("param"); assertTrue(list.size() > 0); } }
2. 这种注入Bean的方式是否正确?
字段注入(@Autowired标注在字段上)是Spring支持的注入方式,但存在几个明显缺点:
- 无法保证依赖非空,容易出现
NullPointerException; - 类的依赖关系不直观,从构造器无法直接看出需要哪些Bean;
- 测试时需要用反射或
@InjectMocks注入依赖,不如构造器注入简洁。
但如果你偏好这种方式简化代码,业务代码中是可以使用的,Spring官方并没有完全禁止,只是更推荐构造器注入。
3. 服务类必须使用构造器注入Bean吗?
不是必须的,但构造器注入是Spring官方推荐的方式,原因如下:
- 可以保证依赖非空(字段用
final修饰时,构造器必须初始化); - 类的依赖关系清晰,通过构造器就能明确知道需要哪些依赖;
- 测试时不需要反射,直接通过构造器传入Mock或真实实例,更直观;
- 符合依赖注入原则,类的依赖由外部提供,而非自己获取。
如果你坚持用字段注入,只要测试时正确处理依赖(比如注入真实实例而非Mock),也能正常完成测试,只是相对麻烦一些。
内容的提问来源于stack exchange,提问作者user2094311

