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

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,提问作者Портретный Дмитрий

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:57:00