如何在Spring Session单元测试中Mock JDBC以规避真实数据库操作
问题:Spring Boot Kotlin应用单元测试如何避免Spring Session操作真实数据库
我正在开发一个基于Spring Boot的Kotlin应用,其中一个Controller通过Spring Session结合JDBC持久化会话ID:
@RestController @RequestMapping("/health") class HealthController { // healthCheck returns true iff the application is healthy. @GetMapping fun healthCheck(session: HttpSession): Map<String, Any> { val resp = LinkedHashMap<String, Any>() resp["status"] = true resp["sessionId"] = session.id println("health check successful") return resp } }
生产环境中,每次调用health端点时,若会话不存在会自动保存到JDBC配置的数据库,这是预期行为,配置如下:
spring: datasource: url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:} username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:password} session: store-type: jdbc
但运行单元测试时,会话数据也会写入真实数据库,测试代码如下:
@SpringBootTest @AutoConfigureMockMvc class HealthControllerTest @Autowired constructor(private val mockMvc: MockMvc) { // TODO(shubham): Modify unit test to use mock DB under the hood instead of configured app DB @Test fun `healthCheck should return status true`() { mockMvc.perform(get("/health")) .andExpect(status().isOk) .andExpect(jsonPath("$.status").value(true)) } }
我尝试Mock JdbcOperations、JdbcTemplate等类来让Spring Session使用Mock,但这些Mock都被忽略,依然操作真实数据库,以下是尝试的方案及错误:
尝试的方案与错误
1. 使用NamedParameterJdbcTemplate + @Mock注解
@SpringBootTest @AutoConfigureMockMvc class HealthControllerTest @Autowired constructor(private val mockMvc: MockMvc) { @Mock private lateinit var template: NamedParameterJdbcTemplate @Test fun `healthCheck should return status true`() { Assertions.assertNotNull(template) `when`(template.update(any(String::class.java), any(MapSqlParameterSource::class.java))) .thenAnswer { 1 } mockMvc.perform(get("/health")) .andExpect(status().isOk) .andExpect(jsonPath("$.status").value(true)) verify(template).update(any(String::class.java), any(MapSqlParameterSource::class.java)) } }
错误信息:
Wanted but not invoked: template.update( <any java.lang.String>, <any org.springframework.jdbc.core.namedparam.MapSqlParameterSource> ); -> at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.update(NamedParameterJdbcTemplate.java:337) Actually, there were zero interactions with this mock.
2. 使用NamedParameterJdbcTemplate + @MockBean注解
仅修改注解为@MockBean:
@MockBean private lateinit var template: NamedParameterJdbcTemplate
错误信息:
Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'userRepository' defined in com.app.server.repository.UserRepository defined in @EnableJdbcRepositories declared on JdbcRepositoriesRegistrar.EnableJdbcRepositoriesConfiguration: Cannot resolve reference to bean 'org.springframework.data.jdbc.core.mapping.JdbcMappingContext' while setting bean property 'mappingContext'
3. 使用JdbcTemplate + @Mock注解
@Mock private lateinit var template: JdbcTemplate
错误信息:
Wanted but not invoked: template.update( <any java.lang.String>, <any org.springframework.jdbc.core.namedparam.MapSqlParameterSource> ); -> at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1024) Actually, there were zero interactions with this mock.
4. 使用JdbcTemplate + @MockBean注解
@MockBean private lateinit var template: JdbcTemplate
错误信息:
Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'userRepository' defined in com.app.server.repository.UserRepository defined in @EnableJdbcRepositories declared on JdbcRepositoriesRegistrar.EnableJdbcRepositoriesConfiguration: Cannot resolve reference to bean 'org.springframework.data.jdbc.core.mapping.JdbcMappingContext' while setting bean property 'mappingContext'
5. 使用JdbcOperations + @Mock注解
@Mock private lateinit var jdbcOperations: JdbcOperations
错误信息:
Wanted but not invoked: jdbcOperations.update( <any java.lang.String>, <any org.springframework.jdbc.core.namedparam.MapSqlParameterSource> ); -> at com.ordrsmart.server.controller.HealthControllerTest.healthCheck should return status true(HealthControllerTest.kt:63) Actually, there were zero interactions with this mock.
6. 使用JdbcOperations + @MockBean注解
@MockBean private lateinit var jdbcOperations: JdbcOperations
错误信息:
*************************** APPLICATION FAILED TO START *************************** Description: Parameter 0 of method namedParameterJdbcTemplate in org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcTemplateConfiguration required a bean of type 'org.springframework.jdbc.core.JdbcTemplate' that could not be found. Action: Consider defining a bean of type 'org.springframework.jdbc.core.JdbcTemplate' in your configuration.
解决方案
方案1:使用内嵌数据库(推荐)
在测试环境配置中使用H2内嵌数据库,完全隔离真实数据库,无需Mock任何类。
- 添加H2依赖到
build.gradle.kts:
// build.gradle.kts testImplementation("com.h2database:h2")
- 创建测试专用配置文件
src/test/resources/application-test.yml:
spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver username: sa password: session: store-type: jdbc
- 修改测试类,指定使用测试配置:
@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") // 激活测试配置 class HealthControllerTest @Autowired constructor(private val mockMvc: MockMvc) { @Test fun `healthCheck should return status true`() { mockMvc.perform(get("/health")) .andExpect(status().isOk) .andExpect(jsonPath("$.status").value(true)) } }
方案2:Mock Spring Session的JDBC存储实现
如果不想用内嵌数据库,可以直接Mock JdbcIndexedSessionRepository(Spring Session JDBC的核心实现类):
@SpringBootTest @AutoConfigureMockMvc class HealthControllerTest @Autowired constructor(private val mockMvc: MockMvc) { @MockBean private lateinit var sessionRepository: JdbcIndexedSessionRepository @Test fun `healthCheck should return status true`() { // 模拟会话查询与保存逻辑 val mockSessionId = "mock-session-id" `when`(sessionRepository.findById(anyString())).thenReturn(Optional.empty()) `when`(sessionRepository.save(any())).thenAnswer { invocation -> val session = invocation.getArgument<Session>(0) session.id = mockSessionId session } mockMvc.perform(get("/health")) .andExpect(status().isOk) .andExpect(jsonPath("$.status").value(true)) .andExpect(jsonPath("$.sessionId").value(mockSessionId)) // 验证会话存储方法被调用 verify(sessionRepository).save(any()) } }
方案3:使用MockHttpSession跳过真实会话存储
如果只是测试Controller的核心逻辑,无需验证会话存储,可以直接传入MockHttpSession,避免触发Spring Session的数据库操作:
@SpringBootTest @AutoConfigureMockMvc class HealthControllerTest @Autowired constructor(private val mockMvc: MockMvc) { @Test fun `healthCheck should return status true`() { val mockSession = MockHttpSession() mockMvc.perform(get("/health") .session(mockSession)) .andExpect(status().isOk) .andExpect(jsonPath("$.status").value(true)) .andExpect(jsonPath("$.sessionId").value(mockSession.id)) } }
内容的提问来源于stack exchange,提问作者Shubham Goenka
相关产品推荐
相关产品推荐

