Kotlin Spring Boot集成测试中Mockito无法Mock Dao层问题求助
你的问题核心在于各层之间都是硬编码实例化对象,而非依赖注入——PersonUseCase里直接new PersonImplementation(),PersonImplementation里又new PersonDaoResponse(),这种情况下Mockito创建的Spy/Mock对象根本没法替换掉业务代码里实际使用的实例,所以单独调用Mock有效,但走Controller的完整链路时就失效了。
下面给你两种解决方案,优先推荐第一种(符合Spring最佳实践):
方案一:重构代码为依赖注入(推荐)
Spring的核心就是依赖注入,把这些手动实例化的类改成Spring管理的Bean,这样测试时就能轻松替换为Mock对象。
1. 改造各层代码
- Dao层:给
PersonDaoResponse加上@Repository或@Component,让Spring管理它:
@Repository class PersonDaoResponse { fun findAll(): List<PersonRaw> { // 原固定数据逻辑 } }
- 实现层:通过构造器注入
PersonDaoResponse,并加上@Component:
@Component class PersonImplementation(private val personDao: PersonDaoResponse) { fun findAll(): List<Person> { return personDao.findAll().map { /* 转换逻辑 */ } } }
- UseCase层:同样构造器注入
PersonImplementation,加上@Component:
@Component class PersonUseCase(private val personImpl: PersonImplementation) { fun getAllPersons(): List<Person> { return personImpl.findAll() } }
- Controller层:保持原注入逻辑(应该已经是依赖注入了):
@RestController class PersonController(private val personUseCase: PersonUseCase) { @GetMapping("/all") fun getAll(): List<Person> { return personUseCase.getAllPersons() } }
2. 编写集成测试
在Serenity+Spring Boot的集成测试中,用@SpringBootTest加载上下文,配合@MockBean来替换PersonDaoResponse:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SerenityTest class PersonControllerIntegrationTest { @Autowired private lateinit var restTemplate: TestRestTemplate @MockBean private lateinit var personDaoResponse: PersonDaoResponse @Test fun `should return mocked data when calling all endpoint`() { // 准备Mock数据 val mockedRawPersons = listOf(PersonRaw("mock-id", "Mock Name")) `when`(personDaoResponse.findAll()).thenReturn(mockedRawPersons) // 调用接口 val response = restTemplate.getForObject("/all", Array<Person>::class.java)?.toList() // 验证结果 assertThat(response).hasSize(1) assertThat(response?.first()?.name).isEqualTo("Mock Name") verify(personDaoResponse, times(1)).findAll() } }
这样整个调用链都会使用Mock的PersonDaoResponse,因为Spring会把MockBean注入到PersonImplementation,再注入到PersonUseCase,最终Controller调用的就是带Mock的实例。
方案二:用反射替换内部实例(临时 workaround)
如果暂时无法重构代码,只能用反射强行替换业务代码里的内部实例,这种方式不优雅,维护成本高,但能临时解决问题:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SerenityTest class PersonControllerIntegrationTest { @Autowired private lateinit var personController: PersonController @Mock private lateinit var mockedPersonDao: PersonDaoResponse @Test fun `should use mocked dao via reflection`() { // 1. 从PersonController获取PersonUseCase实例 val useCaseField = PersonController::class.java.getDeclaredField("personUseCase") useCaseField.isAccessible = true val personUseCase = useCaseField.get(personController) as PersonUseCase // 2. 从PersonUseCase获取PersonImplementation实例 val implField = PersonUseCase::class.java.getDeclaredField("personImpl") implField.isAccessible = true val personImpl = implField.get(personUseCase) as PersonImplementation // 3. 替换PersonImplementation里的PersonDaoResponse为Mock对象 val daoField = PersonImplementation::class.java.getDeclaredField("personDao") daoField.isAccessible = true daoField.set(personImpl, mockedPersonDao) // 4. 配置Mock行为 val mockedRawPersons = listOf(PersonRaw("mock-id", "Mock Name")) `when`(mockedPersonDao.findAll()).thenReturn(mockedRawPersons) // 5. 调用接口验证 val response = personController.getAll() assertThat(response).hasSize(1) assertThat(response.first().name).isEqualTo("Mock Name") } }
注意:这种方式依赖类的字段名,如果后续代码修改字段名,测试就会失效,所以只建议作为临时方案,最终还是要重构为依赖注入。
总结:优先采用依赖注入的方式,这不仅能解决测试Mock的问题,也符合Spring的设计理念,让代码更易于维护和扩展。
内容的提问来源于stack exchange,提问作者Victor Tripeno

