Disable Testcontainers in Spring Boot when running in CI / CD Pipeline
Testcontainers is a great tool for local development and testing, allowing you to spin up containers that mimic the actual environment to test against. It has frameworks for almost every programming language for easy integration.
Sadly, it uses Docker under the hood and does not want to support other container runtimes like Kubernetes. Since many GitLab Runners are hosted inside a Kubernetes cluster or do not want to expose the Docker socket for security reasons, Testcontainers does not work anymore and tests fail in the pipeline.
You cannot disable Testcontainers when Docker is not present because it will disable the whole test and not just the container initialization.
That’s when a real engineer is needed and ChatGPT has to stay at home…
JDBC
How to migrate if you are using the JDBC integration. You might have an application-local.yaml
that looks like this:
spring:
flyway.enabled: true
datasource:
url: "jdbc:tc:postgresql:13.8:///testing"
username: "testing"
password: "testing"
First, we need to create another profile that is not using the Testcontainers integration. Create a file called application-ci.yaml
:
spring:
flyway.enabled: true
datasource:
# postgres:5432 is the url to reach our new database
url: "jdbc:postgresql://postgres:5432/testing"
username: "testing"
password: "testing"
Usually your tests look like this:
@DataJpaTest
// this activates the profile "test" for the current test
@ActiveProfiles("test")
class SomeTest() {}
The profile test
is hardcoded here. Let’s implement a resolver that changes the active profile based on an environment variable:
import org.springframework.test.context.ActiveProfilesResolver
class ProfileResolver : ActiveProfilesResolver {
override fun resolve(testClass: Class<*>): Array<String> {
val env: Map<String, String> = System.getenv()
// read out the profile environment variable
// use TC as default (local development)
val profile: String = env.getOrDefault("PROFILE", "local")
return arrayOf(profile)
}
}
This function returns local
if the PROFILE
environment variable is not set. Otherwise, it returns the value of the variable.
The resolver can be used like this:
@DataJpaTest
// instead of hardcoding the profile we use a resolver here
@ActiveProfiles(resolver = ProfileResolver::class)
class SomeTest() {}
Set the variable in your CI definition. Example in GitLab CI:
test:
services:
# use any container image
- postgres:13.8
variables:
PROFILE: ci
scripts:
- ./gradlew test
Testcontainers API
Sometimes you use the Testcontainers API directly in the code to create containers and use them.
Here is a basic example:
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class SomeIntegrationTest() {
// your tests are here ...
companion object {
@Container
@JvmStatic
private val kafkaContainer =
KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
.withKraft()
@DynamicPropertySource
@JvmStatic
@Suppress("unused")
fun registerProperties(registry: DynamicPropertyRegistry) =
registry.add("spring.kafka.bootstrap-servers") {
kafkaContainer.bootstrapServers
}
}
}
We cannot solve this simply by switching the active profile.
In the first step, you need to implement the resolver from the JDBC section to be able to switch profiles on demand.
Now let’s create a TestConfiguration
that will handle the container creation only when the local
profile is active:
@TestConfiguration
// IMPORTANT: this configuration only runs when "local" profile is active
@Profile("local")
class ContainerConfiguration() {
companion object {
init {
val kafkaContainer =
KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"))
.withKraft()
kafkaContainer.start()
System.setProperty(
"spring.kafka.bootstrap-servers",
kafkaContainer.bootstrapServers,
)
}
}
}
In the last step, use the resolver and configuration in your test case:
@SpringBootTest
@ActiveProfiles(resolver = ProfileResolver::class)
@Import(ContainerConfiguration::class)
class AccountBlockingStartedConsumerIntegrationTest() {
// tests here...
// companion object removed!
}
If the PROFILE
environment variable is set to local
or empty, the container will be created. If it is set to something else, the container won’t be started since the configuration won’t be applied. Make sure to set a correct URL to a running instance in the different application-<profile>.yaml
files. Maybe just use GitLab Services to create the container.