I have wrote some Integration Tests for my OpenLiberty with MicroProfile application. In order for the tests to work I must first execute the libertyDev
command. So I thought it was a good idea to use Testcontainers in which I will try to create an OpenLiberty server container and load it with the proper configuration files. Following this scope my tests are as follows.
package integration.gr.iag.dgtl.inventory
import gr.iag.dgtl.inventory.PropertyReader
import gr.iag.dgtl.inventory.TestItemProvider
import gr.iag.dgtl.inventory.dto.ErrorResponse
import jakarta.json.bind.Jsonb
import jakarta.json.bind.JsonbBuilder
import jakarta.ws.rs.client.Client
import jakarta.ws.rs.client.ClientBuilder
import jakarta.ws.rs.client.Entity
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import org.slf4j.LoggerFactory
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.output.Slf4jLogConsumer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.spock.Testcontainers
import org.testcontainers.utility.DockerImageName
import org.testcontainers.utility.MountableFile
import spock.lang.Shared
import spock.lang.Specification
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
@Testcontainers
class TrackingInventoryIntegrationSpec extends Specification {
@Shared
GenericContainer openLiberty = new GenericContainer(DockerImageName.parse("open-liberty:23.0.0.12-full-java17-openj9"))
.withExposedPorts(9080)
.withCopyFileToContainer(MountableFile.forHostPath("./src/main/liberty/config/tracking-inventory-0.0.1-SNAPSHOT.war"), "/opt/ol/wlp/usr/servers/defaultServer/apps/tracking-inventory-0.0.1-SNAPSHOT.war")
.withCopyFileToContainer(MountableFile.forHostPath("./src/main/liberty/config/server.xml"), "/opt/ol/wlp/usr/servers/defaultServer/server.xml")
.withCopyFileToContainer(MountableFile.forHostPath("./src/main/liberty/config/bootstrap.properties"), "/opt/ol/wlp/usr/servers/defaultServer/bootstrap.properties")
.withCopyFileToContainer(MountableFile.forHostPath("./src/main/liberty/config/GeneratedSSLInclude.xml"), "/opt/ol/wlp/usr/servers/defaultServer/GeneratedSSLInclude.xml")
.withCopyFileToContainer(MountableFile.forHostPath("./src/main/liberty/config/users.xml"), "/opt/ol/wlp/usr/servers/defaultServer/users.xml")
.waitingFor(Wait.forHttp("/").forStatusCode(200))
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("openLiberty")))
@Shared
Jsonb jsonb
def requestBody
@Shared
HttpClient client
def appUrl
def setup() {
openLiberty.start()
int actualPort = openLiberty.getMappedPort(9080)
String host = openLiberty.getHost()
appUrl = "http://$host:$actualPort"
final String allLogs = openLiberty.getLogs()
println("Container Logs: n$allLogs")
client = HttpClient.newHttpClient()
jsonb = JsonbBuilder.create()
requestBody = TestItemProvider.generateRandomItem()
}
def 'Successful create item and persist it into JSON, HTML and CSV'() {
when: 'the call is succeeded'
def response = doPost(jsonb.toJson(requestBody))
then: 'empty response body means successful request'
response.status == Response.Status.CREATED.statusCode
}
def 'Failed to create the item'() {
given: 'an item with null name'
def requestBody = TestItemProvider.createItemWithNullName()
when: 'calling the application with this item'
def response = doPost(jsonb.toJson(requestBody))
then: 'a response with error message is returned'
response.status != Response.Status.CREATED.statusCode
response.readEntity(ErrorResponse.class).errors.size() > 0
}
def 'Successfully get an item'() {
given: 'an item is already created'
doPost(jsonb.toJson(requestBody))
when: 'the call is made to the get api'
def response = doGet()
then: 'the response contains an OK status'
response.status == Response.Status.OK.statusCode
and: 'the response body contains the correct information'
def returnedItemMap = response.readEntity(List.class)[0]
returnedItemMap.name == requestBody.name
returnedItemMap.serialNumber == requestBody.serialNumber
returnedItemMap.value == requestBody.value
}
def 'Successfully delete an item'() {
given: 'an item is already created'
doPost(jsonb.toJson(requestBody))
when: 'the call is made to the delete api'
def response = doDelete(requestBody.serialNumber)
then: 'the response contains an OK status'
response.status == Response.Status.NO_CONTENT.statusCode
}
def doPost(Object requestPayload) {
Client client = ClientBuilder.newClient()
String targetUrl = "$appUrl/tracking-inventory/inventory"
return client.target(targetUrl)
.request(MediaType.APPLICATION_JSON)
.post(Entity.json(requestPayload))
}
def doGet() {
Client client = ClientBuilder.newClient()
String targetUrl = "$appUrl/tracking-inventory/inventory"
return client.target(targetUrl)
.request(MediaType.APPLICATION_JSON)
.get()
}
}
Here I am mounting the war and my server.xml file as well as various configuration files. I know that the WAR file gets placed into build/libs folder and I will try to write a task in my build.gradle file like tests.dependsOn war
, to properly generate it before tests execution.
My problem is that upon running the first test, I am getting the next error:
org.testcontainers.containers.ContainerLaunchException: Container startup failed for image open-liberty:23.0.0.12-full-java17-openj9
at app//org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:359)
at app//org.testcontainers.containers.GenericContainer.start(GenericContainer.java:330)
at org.testcontainers.spock.TestcontainersMethodInterceptor.startContainers_closure3(TestcontainersMethodInterceptor.groovy:83)
at app//groovy.lang.Closure.call(Closure.java:433)
at app//groovy.lang.Closure.call(Closure.java:422)
at app//org.testcontainers.spock.TestcontainersMethodInterceptor.startContainers(TestcontainersMethodInterceptor.groovy:80)
at app//org.testcontainers.spock.TestcontainersMethodInterceptor.interceptSetupSpecMethod(TestcontainersMethodInterceptor.groovy:25)
at app//org.spockframework.runtime.extension.AbstractMethodInterceptor.intercept(AbstractMethodInterceptor.java:36)
at app//org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:101)
at app//org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:156)
at [email protected]/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: org.rnorth.ducttape.RetryCountExceededException: Retry limit hit with exception
at app//org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:88)
at app//org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:344)
... 10 more
Caused by: org.testcontainers.containers.ContainerLaunchException: Could not create/start container
at app//org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:563)
at app//org.testcontainers.containers.GenericContainer.lambda$doStart$0(GenericContainer.java:354)
at app//org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:81)
... 11 more
Caused by: org.testcontainers.containers.ContainerLaunchException: Timed out waiting for URL to be accessible (http://localhost:55240/ should return HTTP [200])
at app//org.testcontainers.containers.wait.strategy.HttpWaitStrategy.waitUntilReady(HttpWaitStrategy.java:320)
at app//org.testcontainers.containers.wait.strategy.AbstractWaitStrategy.waitUntilReady(AbstractWaitStrategy.java:52)
at app//org.testcontainers.containers.GenericContainer.waitUntilContainerStarted(GenericContainer.java:909)
at app//org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:500)
... 13 more
Caused by: org.rnorth.ducttape.TimeoutException: Timeout waiting for result with exception
at app//org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:54)
at app//org.testcontainers.containers.wait.strategy.HttpWaitStrategy.waitUntilReady(HttpWaitStrategy.java:252)
... 16 more
Caused by: java.lang.RuntimeException: java.net.SocketException: Unexpected end of file from server
at org.testcontainers.containers.wait.strategy.HttpWaitStrategy.lambda$null$6(HttpWaitStrategy.java:312)
at org.rnorth.ducttape.ratelimits.RateLimiter.doWhenReady(RateLimiter.java:27)
at org.testcontainers.containers.wait.strategy.HttpWaitStrategy.lambda$waitUntilReady$7(HttpWaitStrategy.java:257)
at org.rnorth.ducttape.unreliables.Unreliables.lambda$retryUntilSuccess$0(Unreliables.java:43)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:857)
Caused by: java.net.SocketException: Unexpected end of file from server
at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:954)
at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:761)
at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:951)
at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:761)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1688)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1589)
at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529)
at org.testcontainers.containers.wait.strategy.HttpWaitStrategy.lambda$null$6(HttpWaitStrategy.java:276)
... 7 more
The container logs are the following:
WARNING: Unknown module: jdk.management.agent specified to --add-exports
WARNING: Unknown module: jdk.attach specified to --add-exports
Launching defaultServer (Open Liberty 23.0.0.12/wlp-1.0.84.cl231220231127-1901) on Eclipse OpenJ9 VM, version 17.0.10+7 (en_US)
[AUDIT ] CWWKE0001I: The server defaultServer has been launched.
[AUDIT ] CWWKG0093A: Processing configuration drop-ins resource: /opt/ol/wlp/usr/servers/defaultServer/configDropins/defaults/keystore.xml
[AUDIT ] CWWKG0093A: Processing configuration drop-ins resource: /opt/ol/wlp/usr/servers/defaultServer/configDropins/defaults/open-default-port.xml
[AUDIT ] CWWKG0028A: Processing included configuration resource: /opt/ol/wlp/usr/servers/defaultServer/users.xml
[AUDIT ] CWWKG0028A: Processing included configuration resource: /opt/ol/wlp/usr/servers/defaultServer/GeneratedSSLInclude.xml
[AUDIT ] CWWKG0102I: Found conflicting settings for defaultKeyStore instance of keyStore configuration.
Property password has conflicting values:
Secure value is set in file:/opt/ol/wlp/usr/servers/defaultServer/configDropins/defaults/keystore.xml.
Secure value is set in file:/opt/ol/wlp/usr/servers/defaultServer/GeneratedSSLInclude.xml.
Property password will be set to the value defined in file:/opt/ol/wlp/usr/servers/defaultServer/GeneratedSSLInclude.xml.
[AUDIT ] CWWKG0102I: Found conflicting settings for defaultHttpEndpoint instance of httpEndpoint configuration.
Property host has conflicting values:
Value * is set in file:/opt/ol/wlp/usr/servers/defaultServer/configDropins/defaults/open-default-port.xml.
Value localhost is set in file:/opt/ol/wlp/usr/servers/defaultServer/server.xml.
Property host will be set to localhost.
[AUDIT ] CWWKZ0058I: Monitoring dropins for applications.
[AUDIT ] CWWKS4104A: LTPA keys created in 0.206 seconds. LTPA key file: /opt/ol/wlp/output/defaultServer/resources/security/ltpa.keys
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/jwt/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/openapi/platform/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/openapi/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/IBMJMXConnectorREST/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/metrics/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/health/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/openapi/ui/
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/ibm/api/
[AUDIT ] CWPKI0803A: SSL certificate created in 1.513 seconds. SSL key file: /opt/ol/wlp/output/defaultServer/resources/security/key.p12
[AUDIT ] CWWKI0001I: The CORBA name server is now available at corbaloc:iiop:localhost:2809/NameService.
[AUDIT ] CWWKT0016I: Web application available (default_host): http://localhost:9080/
[AUDIT ] CWWKZ0001I: Application tracking-inventory-0.0.1-SNAPSHOT started in 2.275 seconds.
[AUDIT ] CWWKF1037I: The server added the [appAuthentication-2.0, appAuthorization-2.0, appClientSupport-2.0, appSecurity-4.0, batch-2.0, beanValidation-3.0, cdi-3.0, concurrent-2.0, connectors-2.0, connectorsInboundSecurity-2.0, enterpriseBeans-4.0, enterpriseBeansHome-4.0, enterpriseBeansLite-4.0, enterpriseBeansPersistentTimer-4.0, enterpriseBeansRemote-4.0, expressionLanguage-4.0, faces-3.0, jakartaee-9.1, json-1.0, jsonb-2.0, jsonp-2.0, jwt-1.0, localConnector-1.0, mail-2.0, managedBeans-2.0, mdb-4.0, messaging-3.0, messagingClient-3.0, messagingSecurity-3.0, messagingServer-3.0, microProfile-5.0, monitor-1.0, mpConfig-3.0, mpFaultTolerance-4.0, mpHealth-4.0, mpJwt-2.0, mpMetrics-4.0, mpOpenAPI-3.0, mpOpenTracing-3.0, mpRestClient-3.0, pages-3.0, persistence-3.0, persistenceContainer-3.0, restConnector-2.0, restfulWS-3.0, restfulWSClient-3.0, servlet-5.0, transportSecurity-1.0, webProfile-9.1, websocket-2.0, xmlBinding-3.0, xmlWS-3.0] features to the existing feature set.
[AUDIT ] CWWKF0012I: The server installed the following features: [appAuthentication-2.0, appAuthorization-2.0, appClientSupport-2.0, appSecurity-4.0, batch-2.0, beanValidation-3.0, cdi-3.0, concurrent-2.0, connectors-2.0, connectorsInboundSecurity-2.0, distributedMap-1.0, enterpriseBeans-4.0, enterpriseBeansHome-4.0, enterpriseBeansLite-4.0, enterpriseBeansPersistentTimer-4.0, enterpriseBeansRemote-4.0, expressionLanguage-4.0, faces-3.0, jakartaee-9.1, jdbc-4.2, jndi-1.0, json-1.0, jsonb-2.0, jsonp-2.0, jwt-1.0, localConnector-1.0, mail-2.0, managedBeans-2.0, mdb-4.0, messaging-3.0, messagingClient-3.0, messagingSecurity-3.0, messagingServer-3.0, microProfile-5.0, monitor-1.0, mpConfig-3.0, mpFaultTolerance-4.0, mpHealth-4.0, mpJwt-2.0, mpMetrics-4.0, mpOpenAPI-3.0, mpOpenTracing-3.0, mpRestClient-3.0, pages-3.0, persistence-3.0, persistenceContainer-3.0, restConnector-2.0, restfulWS-3.0, restfulWSClient-3.0, servlet-5.0, ssl-1.0, transportSecurity-1.0, webProfile-9.1, websocket-2.0, xmlBinding-3.0, xmlWS-3.0].
[AUDIT ] CWWKF0013I: The server removed the following features: [appClientSupport-1.0, appSecurity-2.0, appSecurity-3.0, batch-1.0, beanValidation-2.0, cdi-2.0, concurrent-1.0, ejb-3.2, ejbHome-3.2, ejbLite-3.2, ejbPersistentTimer-3.2, ejbRemote-3.2, el-3.0, j2eeManagement-1.1, jacc-1.5, jaspic-1.1, javaMail-1.6, javaee-8.0, jaxb-2.2, jaxrs-2.1, jaxrsClient-2.1, jaxws-2.2, jca-1.7, jcaInboundSecurity-1.0, jms-2.0, jpa-2.2, jpaContainer-2.2, jsf-2.3, jsonb-1.0, jsonp-1.1, jsp-2.3, managedBeans-1.0, mdb-3.2, servlet-4.0, wasJmsClient-2.0, wasJmsSecurity-1.0, wasJmsServer-1.0, webProfile-8.0, websocket-1.1].
[AUDIT ] CWWKF0011I: The defaultServer server is ready to run a smarter planet. The defaultServer server started in 6.601 seconds.
I tried to run the image outside my tests using the docker run --rm -p 9080:9080 open-liberty:23.0.0.12-full-java17-openj9
command and then check the http://localhost:9080
and it is working as expected. Why am I losing connection with the server? What am I missing?
2
Answers
May I ask how you run the test? Run it inside the Liberty dev mode (you mentioned libertyDev) by hitting the enter command, or run it outside, e.g. gradle test, without a running Liberty.
If inside dev mode, I doubt that there is port conflict between the Liberty started by dev mode and the Liberty started by the testcontainers. From your description, I guess both use 9080. For this issue, you may see this guide how to support running testcontainers test inside and outside dev mode (although the guide uses Maven).
My code in Java: