Kotlin Spring Boot中JUnit 5单元测试隔离失败问题求助
Kotlin Spring Boot单元测试隔离失效问题解决
环境依赖
项目使用的build.gradle.kts配置如下:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.4.3" id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") version "1.5.31" kotlin("plugin.spring") version "1.5.31" kotlin("plugin.jpa") version "1.5.31" id("org.jmailen.kotlinter") version "3.2.0" } group = "com.example" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_15 repositories { maven { url = uri("custom_repository_url") } } val springDocVersion = "1.3.9" val testcontainersVersion = "1.16.0" dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("io.micrometer:micrometer-registry-prometheus") implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework:spring-test") implementation("com.vladmihalcea:hibernate-types-52:2.10.3") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.bouncycastle:bcpkix-jdk15on:1.65") implementation("io.jsonwebtoken:jjwt:0.9.1") implementation("org.springframework.boot:spring-boot-starter-web") { exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat") } implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.boot:spring-boot-starter-jetty") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.liquibase:liquibase-core") implementation("org.apache.poi:poi:5.0.0") implementation("org.apache.poi:poi-ooxml:5.0.0") runtimeOnly("org.postgresql:postgresql") implementation("org.springdoc:springdoc-openapi-ui:$springDocVersion") implementation("org.springdoc:springdoc-openapi-kotlin:$springDocVersion") implementation("org.springdoc:springdoc-openapi-data-rest:$springDocVersion") implementation("io.github.microutils:kotlin-logging:1.7.9") implementation("io.projectreactor:reactor-core") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") exclude(module = "mockito-core") } testImplementation("org.springframework.amqp:spring-rabbit-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.testcontainers:testcontainers:$testcontainersVersion") testImplementation("org.testcontainers:postgresql:$testcontainersVersion") testImplementation("org.testcontainers:rabbitmq:$testcontainersVersion") testImplementation("io.mockk:mockk:1.9.3") testImplementation("com.ninja-squad:springmockk:3.0.1") implementation("net.javacrumbs.shedlock:shedlock-spring:2.1.0") implementation("net.javacrumbs.shedlock:shedlock-provider-jdbc-template:2.1.0") } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "15" } } tasks.withType<Test> { useJUnitPlatform() systemProperties = mapOf( "spring.profiles.active" to "test" ) } tasks.test { jvmArgs("--add-opens", "java.base/java.time=ALL-UNNAMED") }
问题场景
服务类代码:
import org.springframework.stereotype.Service @Service class SomeService( private val utilService: UtilService, private val rabbitTemplate: RabbitTemplate ) { // 业务方法略 }
测试类代码:
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.MethodOrderer.OrderAnnotation import io.mockk.every import io.mockk.mockk import io.mockk.verify @TestMethodOrder(OrderAnnotation::class) class SomeServiceTest { private val utilService: UtilService = mockk() private val rabbitTemplate: RabbitTemplate = mockk() private val subject = SomeService( utilService = utilService, rabbitTemplate = rabbitTemplate ) @Order(1) @Test fun `first test`() { every { utilService.returnString() } returns "firstString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } // 其他断言 } @Order(2) @Test fun `second test`() { every { utilService.returnString() } returns "secondString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } // 其他断言 } }
问题现象
单独运行单个测试方法全部通过,但执行gradlew clean build或整个测试类时,测试失败:
- 第二个测试中
utilService.returnString()返回值仍是第一个测试设置的"firstString",而非预期的"secondString" rabbitTemplate.convertAndSend()的调用次数被统计为2次,而非每个测试预期的1次
问题原因
测试类中的mockk()对象和subject是类级别变量,只会在测试类初始化时创建一次。MockK的mock对象默认会保留之前测试的存根(every设置)和验证状态,导致测试之间状态污染,隔离失效。
解决方案
方案1:每个测试方法重新创建mock和subject
把mock对象和subject的初始化移到每个测试方法内部,确保每次测试都使用全新的实例:
@TestMethodOrder(OrderAnnotation::class) class SomeServiceTest { @Order(1) @Test fun `first test`() { val utilService: UtilService = mockk() val rabbitTemplate: RabbitTemplate = mockk() val subject = SomeService(utilService, rabbitTemplate) every { utilService.returnString() } returns "firstString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } @Order(2) @Test fun `second test`() { val utilService: UtilService = mockk() val rabbitTemplate: RabbitTemplate = mockk() val subject = SomeService(utilService, rabbitTemplate) every { utilService.returnString() } returns "secondString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } }
方案2:使用JUnit 5的@BeforeEach重置mock和subject
通过@BeforeEach注解,在每个测试方法执行前重新初始化mock和subject,避免重复代码:
import org.junit.jupiter.api.BeforeEach // 其他导入略 @TestMethodOrder(OrderAnnotation::class) class SomeServiceTest { private lateinit var utilService: UtilService private lateinit var rabbitTemplate: RabbitTemplate private lateinit var subject: SomeService @BeforeEach fun setUp() { utilService = mockk() rabbitTemplate = mockk() subject = SomeService(utilService, rabbitTemplate) } @Order(1) @Test fun `first test`() { every { utilService.returnString() } returns "firstString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } @Order(2) @Test fun `second test`() { every { utilService.returnString() } returns "secondString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } }
方案3:保留类级别变量,重置mock状态
如果需要保留类级别变量,可以在@BeforeEach中调用MockK的clearMocks()方法,清除之前测试的存根和验证状态:
import io.mockk.clearMocks // 其他导入略 @TestMethodOrder(OrderAnnotation::class) class SomeServiceTest { private val utilService: UtilService = mockk() private val rabbitTemplate: RabbitTemplate = mockk() private val subject = SomeService(utilService, rabbitTemplate) @BeforeEach fun resetMocks() { clearMocks(utilService, rabbitTemplate) } @Order(1) @Test fun `first test`() { every { utilService.returnString() } returns "firstString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } @Order(2) @Test fun `second test`() { every { utilService.returnString() } returns "secondString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } }
方案4:使用SpringMockK注解
如果测试需要Spring上下文支持,可以用@MockK注解配合@ExtendWith(SpringExtension::class),SpringMockK会自动在测试之间隔离mock状态:
import com.ninja.squad.springmockk.MockK import org.junit.jupiter.api.extension.ExtendWith import org.springframework.test.context.junit.jupiter.SpringExtension // 其他导入略 @ExtendWith(SpringExtension::class) @TestMethodOrder(OrderAnnotation::class) class SomeServiceTest { @MockK private lateinit var utilService: UtilService @MockK private lateinit var rabbitTemplate: RabbitTemplate private val subject = SomeService(utilService, rabbitTemplate) @Order(1) @Test fun `first test`() { every { utilService.returnString() } returns "firstString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } @Order(2) @Test fun `second test`() { every { utilService.returnString() } returns "secondString" // 执行业务方法略 verify(exactly = 1) { rabbitTemplate.convertAndSend() } } }
内容的提问来源于stack exchange,提问作者Портретный Дмитрий
相关产品推荐
相关产品推荐

