Search code examples
spring-bootkotlinhibernatejunit5java-17

Springboot + Junit5 application fails to load ApplicationContext in unit tests on CI runner but works locally


I have written a simple test to check for springboot application context and it executes perfectly fine on my local machine using android studio (on windows) and the test passes. I can also see the data being added to the database when I set breakpoints and pause execution. However when the tests are executed in a CI runner using github actions to validate the pull request it fails to load the application context and gives the following error:

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)

Code:

application.yml for the test

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)
    }
}

My local setup

I am using Android studio to develop the code with a gradle based project setup.

OS is Windows 10 and Java version is 17.0.9 according to javac --version

I have tried to execute the github action locally via https://github.com/nektos/act to ensure the same environment as the CI pipeline and the issue can be reproduced locally as well. So my hunch is that my android studio has some difference in settings/environment than the CI runner which I am unable to figure out.

EDIT: Updated code for brevity.


Solution

  • I figured out myself what the issue was in this case. It was occurring because the application was not able to connect to the database on the host. I had to specifically ensure that Postgres was available on the host using a postgres service container as shown here https://docs.github.com/en/actions/using-containerized-services/creating-postgresql-service-containers.