You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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任何类。

  1. 添加H2依赖到build.gradle.kts:
// build.gradle.kts
testImplementation("com.h2database:h2")
  1. 创建测试专用配置文件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
  1. 修改测试类,指定使用测试配置:
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 22:45:03