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

SpringBoot+JUnit5单元测试CI环境加载ApplicationContext失败,本地正常

SpringBoot单元测试在CI环境无法加载ApplicationContext的问题排查

我编写了一个用于检查SpringBoot应用上下文的简单单元测试,在Windows系统的Android Studio本地运行时完全正常,测试通过,断点调试也能看到数据写入数据库。但在GitHub Actions的CI runner中执行PR验证测试时,却无法加载ApplicationContext,抛出如下异常:

java.lang.IllegalStateException: ApplicationContext failure threshold (1) exceeded: skipping repeated attempt to load context for [WebMergedContextConfiguration@7367f3ce testClass = proj.rpc.database.entities.InventoryEntityUnitTest, locations = [], classes = [proj.App], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = ["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true"], contextCustomizers = [org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@41bcbe97, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@bd9c61a, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@23142e20, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@cdd28d1, org.springframework.boot.test.context.SpringBootTestAnnotation@8daa8220], resourceBasePath = "src/main/webapp", contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:145)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:130)
at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:191)
at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:130)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:163)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:310)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

相关代码

测试用application.yml

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/testcopilot
    username: mak
    password: ${POSTGRES_USER_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect

App.kt

package proj

import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean

@SpringBootApplication
class App {
    @Bean
    fun commandLineRunner(ctx: ApplicationContext): CommandLineRunner? {
        return CommandLineRunner {
            // do nothing
        }
    }
}

fun main(args: Array<String>) {
    runApplication<App>(*args)
}

UserEntity.kt

package proj.rpc.database.entities

import com.fasterxml.jackson.annotation.JsonIgnore
import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import java.time.LocalDate

@Entity
@Table(name = "users")
open class UserEntity : Persistable<Long> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    internal var id: Long? = null

    @Column(unique = true)
    private var email: String? = null

    internal constructor()

    constructor(
        email: String
    ) {
        this.email = email
    }

    override fun getId(): Long? {
        return id
    }

    override fun isNew(): Boolean {
        return id == null
    }

    open fun setEmail(email: String) {
        this.email = email
    }

    open fun getEmail(): String? {
        return email
    }
}

UserRepository.kt

package proj.rpc.database.repositories

import proj.rpc.database.entities.UserEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface UserRepository : JpaRepository<UserEntity, Int>

UserRepoUnitTest.kt

package proj.rpc.database.entities

import proj.rpc.database.repositories.UserRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate

@SpringBootTest
@Transactional
class UserRepoUnitTest {
    @Autowired
    private lateinit var userRepo: UserRepository

    @Test
    fun contextLoads(context: ApplicationContext?) {
        assertNotNull(context)
    }
}

本地环境

使用Android Studio开发,Gradle项目,系统为Windows10,Java版本为17.0.9(通过javac --version查看)。用nektos/act在本地模拟CI环境时,同样会复现该问题,推测是Android Studio与CI runner的环境/配置差异导致。

排查与解决方案

1. 启动CI环境中的PostgreSQL服务

本地环境运行了PostgreSQL,但CI runner默认没有启动该服务,导致应用无法连接数据库,上下文加载失败。

  • 解决:在GitHub Actions的Workflow配置中添加PostgreSQL服务:
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: mak
          POSTGRES_PASSWORD: test_pass_123
          POSTGRES_DB: testcopilot
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

同时确保POSTGRES_USER_PASSWORD环境变量在CI中正确设置,可通过GitHub Secrets或Workflow直接指定。

2. 修正JPA仓库泛型类型不匹配问题

UserRepository定义为JpaRepository<UserEntity, Int>,但UserEntity的ID类型是Long,泛型参数不匹配可能导致上下文加载失败。本地环境可能因缓存未触发问题,但CI环境会严格校验。

  • 解决:将UserRepository修正为:
@Repository
interface UserRepository : JpaRepository<UserEntity, Long>

3. 检查并统一Java版本

本地使用Java 17.0.9,需确认CI runner的Java版本是否一致,版本差异可能引发兼容性问题。

  • 解决:在Workflow中指定Java 17版本:
- name: Set up JDK 17
  uses: actions/setup-java@v4
  with:
    java-version: '17'
    distribution: 'temurin'

4. 获取详细错误日志

异常仅提示上下文加载阈值超限,需查看首次加载失败的具体原因。

  • 解决:在CI的Gradle测试任务中添加--info参数,输出详细日志:
- name: Run tests
  run: ./gradlew test --info

内容的提问来源于stack exchange,提问作者Moazzam Abdullah Khan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 20:00:35