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 {
…