I have created a Github repo that can be used to reproduce the error: https://github.com/magnus-larsson/sb31-nativetest-demo.

The sample code contains a test that uses the new support for testcontainers together with Postgresql.

Running tests that use testcontainers for Postgresql works fine:

./gradlew clean test

Building a jar file and a native image and running them with a Postregsql db in Docker also works fine:

docker-compose up -d
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost/bp
export SPRING_DATASOURCE_USERNAME=bp
export SPRING_DATASOURCE_PASSWORD=bp

./gradlew build
java -jar build/libs/tcdemo-0.0.1-SNAPSHOT.jar
curl localhost:8080/customers
CTRL/C

./gradlew nativeBuild
java -jar build/libs/tcdemo-0.0.1-SNAPSHOT.jar
curl localhost:8080/customers
CTRL/C

docker-compose down

But, when running native tests, they fail:

./gradlew clean nativeTest

Error message:

Failures (1):
  JUnit Jupiter:TcdemoApplicationTests
    ClassSource [className = 'com.example.tcdemo.TcdemoApplicationTests', filePosition = null]
    => java.lang.ExceptionInInitializerError
       org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:113)
       org.junit.jupiter.engine.execution.ExtensionValuesStore.lambda$getOrComputeIfAbsent$4(ExtensionValuesStore.java:86)
       org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.computeValue(ExtensionValuesStore.java:223)
       org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.get(ExtensionValuesStore.java:211)
       org.junit.jupiter.engine.execution.ExtensionValuesStore$StoredValue.evaluate(ExtensionValuesStore.java:191)
       [...]
       Suppressed: java.lang.NoClassDefFoundError: Could not initialize class org.springframework.test.context.BootstrapUtils
         org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:113)
         org.junit.jupiter.engine.execution.ExtensionValuesStore.lambda$getOrComputeIfAbsent$4(ExtensionValuesStore.java:86)
         org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.computeValue(ExtensionValuesStore.java:223)
         org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.get(ExtensionValuesStore.java:211)
         org.junit.jupiter.engine.execution.ExtensionValuesStore$StoredValue.evaluate(ExtensionValuesStore.java:191)
         [...]
     Caused by: java.lang.IllegalStateException: Failed to load class for @org.springframework.test.context.web.WebAppConfiguration
       org.springframework.test.context.BootstrapUtils.loadWebAppConfigurationClass(BootstrapUtils.java:213)
       org.springframework.test.context.BootstrapUtils.<clinit>(BootstrapUtils.java:63)
       [...]
     Caused by: java.lang.ClassNotFoundException: org.springframework.test.context.web.WebAppConfiguration
       java.base@17.0.6/java.lang.Class.forName(DynamicHub.java:1132)
       org.springframework.util.ClassUtils.forName(ClassUtils.java:284)
       org.springframework.test.context.BootstrapUtils.loadWebAppConfigurationClass(BootstrapUtils.java:209)
       [...]

Comment From: 1713612859

please check yml.properties

Comment From: wilkinsona

AOT processing of TcdemoApplicationTests fails:

2023-05-30T09:49:18.719+01:00  WARN 52482 --- [           main] o.s.t.c.aot.TestContextAotGenerator      : Failed to generate AOT artifacts for test classes [com.example.tcdemo.TcdemoApplicationTests]

org.springframework.test.context.aot.TestContextAotException: Failed to process test class [com.example.tcdemo.TcdemoApplicationTests] for AOT
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:238) ~[spring-test-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestContextAotGenerator.lambda$processAheadOfTime$4(TestContextAotGenerator.java:204) ~[spring-test-6.0.9.jar:6.0.9]
        at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[na:na]
        at org.springframework.util.MultiValueMapAdapter.forEach(MultiValueMapAdapter.java:179) ~[spring-core-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:196) ~[spring-test-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:158) ~[spring-test-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestAotProcessor.performAotProcessing(TestAotProcessor.java:91) ~[spring-test-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestAotProcessor.doProcess(TestAotProcessor.java:72) ~[spring-test-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestAotProcessor.doProcess(TestAotProcessor.java:39) ~[spring-test-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.AbstractAotProcessor.process(AbstractAotProcessor.java:82) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.boot.test.context.SpringBootTestAotProcessor.main(SpringBootTestAotProcessor.java:63) ~[spring-boot-test-3.1.0.jar:3.1.0]
Caused by: java.lang.IllegalArgumentException: Code generation is not supported for bean definitions declaring an instance supplier callback : Root bean: class [org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerConnectionDetailsFactory$JdbcContainerConnectionDetails]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null
        at org.springframework.beans.factory.aot.BeanDefinitionMethodGenerator.<init>(BeanDefinitionMethodGenerator.java:82) ~[spring-beans-6.0.9.jar:6.0.9]
        at org.springframework.beans.factory.aot.BeanDefinitionMethodGeneratorFactory.getBeanDefinitionMethodGenerator(BeanDefinitionMethodGeneratorFactory.java:100) ~[spring-beans-6.0.9.jar:6.0.9]
        at org.springframework.beans.factory.aot.BeanDefinitionMethodGeneratorFactory.getBeanDefinitionMethodGenerator(BeanDefinitionMethodGeneratorFactory.java:115) ~[spring-beans-6.0.9.jar:6.0.9]
        at org.springframework.beans.factory.aot.BeanRegistrationsAotProcessor.processAheadOfTime(BeanRegistrationsAotProcessor.java:49) ~[spring-beans-6.0.9.jar:6.0.9]
        at org.springframework.beans.factory.aot.BeanRegistrationsAotProcessor.processAheadOfTime(BeanRegistrationsAotProcessor.java:37) ~[spring-beans-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.BeanFactoryInitializationAotContributions.getContributions(BeanFactoryInitializationAotContributions.java:67) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.BeanFactoryInitializationAotContributions.<init>(BeanFactoryInitializationAotContributions.java:49) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.BeanFactoryInitializationAotContributions.<init>(BeanFactoryInitializationAotContributions.java:44) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.ApplicationContextAotGenerator.lambda$processAheadOfTime$0(ApplicationContextAotGenerator.java:58) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.ApplicationContextAotGenerator.withCglibClassHandler(ApplicationContextAotGenerator.java:67) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.context.aot.ApplicationContextAotGenerator.processAheadOfTime(ApplicationContextAotGenerator.java:53) ~[spring-context-6.0.9.jar:6.0.9]
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:234) ~[spring-test-6.0.9.jar:6.0.9]
        ... 10 common frames omitted

This leads to incomplete runtime hints and the subsequent ClassNotFoundException: org.springframework.test.context.web.WebAppConfiguration.

@sbrannen I wonder if AOT processing of tests should fail fast in this situation? If AOT processing has failed, running the tests is very unlikely to succeed and it's easy to miss the earlier error, particularly when the subsequent failure is apparently unrelated.

Comment From: wilkinsona

With a local change in place to work around the AOT processing failure, Testcontainers itself does not work with native tests. They fail when trying to bind ~/.docker/config.json into a DockerConfigFile instance:

Caused by: org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of org.testcontainers.shaded.com.github.dockerjava.core.DockerConfigFile: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: /Users/awilkinson/.docker/config.json; line: 2, column: 2]
       org.testcontainers.shaded.com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1456)
       org.testcontainers.shaded.com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1012)
       org.testcontainers.shaded.com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1206)
       org.testcontainers.shaded.com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:314)
       org.testcontainers.shaded.com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:148)
       [...]

There is reachability metadata for it but it appears to be incomplete. The metadata is for Testcontainers 1.17.6 and the above failure occurs with 1.18.0 so the problem may be have been introduced in Testcontainers 1.18 or the tests for the reachability metadata may not drive this code path.

With the AOT processing workaround in place, nativeTest succeeds with the following additional reflection config:

  {
    "name": "org.testcontainers.shaded.com.github.dockerjava.core.DockerConfigFile",
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true
  }

Comment From: wilkinsona

I've opened https://github.com/oracle/graalvm-reachability-metadata/pull/301 to update the reachability metadata so that ~/.docker/config.json can be deserialised into a DockerConfigFile instance.

Comment From: magnus-larsson

Hello @wilkinsona, and thanks for your support! I noticed that your PR to the reachability project has been approved and merged, great!

How can I test if your PR helps with the problem reported in this issue?

Comment From: wilkinsona

You can't I'm afraid. We need to make a change in Boot before you'll reach the point where the Testcontainers problem occurs.

Comment From: magnus-larsson

Ok, thanks for the update!

Any tentative ideas for what Spring Boot version will get the change implemented?

Comment From: magnus-larsson

Hello @wilkinsona, and thanks for the bug fix!

I tried it out using Spring Boot 3.1.1-SNAPSHOT.

Now I get the error message you referred to above:

Caused by: org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of org.testcontainers.shaded.com.github.dockerjava.core.DockerConfigFile: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: /Users/magnus/.docker/config.json; line: 2, column: 3]

Will https://github.com/oracle/graalvm-reachability-metadata/pull/301 resolve this or what needs to be done before the ./gradlew clean nativeTest works with my sample code?

Comment From: wilkinsona

Yes, https://github.com/oracle/graalvm-reachability-metadata/pull/301 should resolve this. Until the reachability metadata is released, you could provide similar hints yourself:

    static class TestcontainersRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.reflection().registerType(DockerConfigFile.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                    MemberCategory.INVOKE_DECLARED_METHODS);
        }

    }

This registrar can then be imported by your test class:

@ImportRuntimeHints(TestcontainersRuntimeHints.class)
class TcdemoApplicationTests {
…