You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2024/10/13 14:49:11 UTC

(httpcomponents-client) 01/01: Migrated Docker based integration tests to TestContainers

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch testcontainers
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit fb8ab955273d247c2272d578ae3fd138288cb712
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Oct 13 12:28:05 2024 +0200

    Migrated Docker based integration tests to TestContainers
---
 .github/workflows/maven.yml                        |   2 +-
 httpclient5-testing/docker/.dockerignore           |  16 --
 httpclient5-testing/docker/apache-httpd/Dockerfile |  45 ----
 httpclient5-testing/docker/squid/Dockerfile        |  29 ---
 httpclient5-testing/pom.xml                        |  35 +++
 .../CachingHttpAsyncClientCompatibilityTest.java   |   2 +-
 .../CachingHttpClientCompatibilityTest.java        |   2 +-
 .../testing/compatibility/ContainerImages.java     | 104 ++++++++
 .../HttpAsyncClientCompatibilityTest.java          |   2 +-
 .../compatibility/HttpClientCompatibilityIT.java   |  84 +++++++
 .../compatibility/HttpClientCompatibilityTest.java | 194 +++++++++++++++
 .../hc/client5/testing/compatibility/Result.java   |  80 ++++++
 .../testing/extension/sync/HttpClientResource.java |  83 +++++++
 .../extension/sync/TestClientResources.java        |   7 -
 .../external/HttpClientCompatibilityTest.java      | 269 ---------------------
 .../test/resources/docker/httpd}/BUILDING.txt      |  23 --
 .../test/resources/docker/httpd}/httpd-ssl.conf    |   0
 .../test/resources/docker/httpd}/httpd.conf        |   0
 .../test/resources/docker/httpd}/index.txt         |   0
 .../test/resources/docker/httpd}/openssl.cnf       |   0
 .../test/resources/docker/httpd}/serial            |   0
 .../test/resources/docker/httpd}/server-cert.pem   |   0
 .../test/resources/docker/httpd}/server-key.pem    |   0
 .../test/resources}/docker/squid/squid.conf        |   0
 pom.xml                                            |  12 +-
 25 files changed, 595 insertions(+), 394 deletions(-)

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 187832dab..6a88469d5 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -49,4 +49,4 @@ jobs:
         distribution: 'temurin'
         java-version: ${{ matrix.java }}
     - name: Build with Maven
-      run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -P-use-toolchains
+      run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -P-use-toolchains,docker
diff --git a/httpclient5-testing/docker/.dockerignore b/httpclient5-testing/docker/.dockerignore
deleted file mode 100644
index 2b0b58d56..000000000
--- a/httpclient5-testing/docker/.dockerignore
+++ /dev/null
@@ -1,16 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-*/*/.svn
diff --git a/httpclient5-testing/docker/apache-httpd/Dockerfile b/httpclient5-testing/docker/apache-httpd/Dockerfile
deleted file mode 100644
index 6400aff4f..000000000
--- a/httpclient5-testing/docker/apache-httpd/Dockerfile
+++ /dev/null
@@ -1,45 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-FROM httpd:2.4
-MAINTAINER dev@hc.apache.org
-
-ENV httpd_home /usr/local/apache2
-ENV var_dir /var/httpd
-ENV www_dir ${var_dir}/www
-ENV private_dir ${www_dir}/private
-
-RUN apt-get update
-RUN apt-get install -y subversion
-
-RUN mkdir -p ${var_dir}
-RUN svn co --depth immediates http://svn.apache.org/repos/asf/httpcomponents/site ${www_dir}
-RUN svn up --set-depth infinity ${www_dir}/images
-RUN svn up --set-depth infinity ${www_dir}/css
-
-RUN mkdir ${httpd_home}/ssl
-COPY server-cert.pem ${httpd_home}/ssl/
-COPY server-key.pem ${httpd_home}/ssl/
-COPY httpd.conf ${httpd_home}/conf/
-COPY httpd-ssl.conf ${httpd_home}/conf/extra/
-
-RUN mkdir -p ${private_dir}
-# user: testuser; pwd: nopassword
-RUN echo "testuser:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=" > ${private_dir}/.htpasswd
-RUN echo "testuser:Restricted Files:73deccd22e07066db8c405e5364335f5" > ${private_dir}/.htpasswd_digest
-RUN echo "Big Secret" > ${private_dir}/big-secret.txt
-
-EXPOSE 8080
-EXPOSE 8443
diff --git a/httpclient5-testing/docker/squid/Dockerfile b/httpclient5-testing/docker/squid/Dockerfile
deleted file mode 100644
index 60f4253e6..000000000
--- a/httpclient5-testing/docker/squid/Dockerfile
+++ /dev/null
@@ -1,29 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-FROM sameersbn/squid:3.3.8-22
-MAINTAINER dev@hc.apache.org
-
-ENV conf_dir /etc/squid3
-
-RUN apt-get update
-RUN apt-get install -y apache2-utils
-
-COPY squid.conf ${conf_dir}/
-
-RUN htpasswd -b -c ${conf_dir}/htpasswd squid nopassword
-
-EXPOSE 8888
-EXPOSE 8889
diff --git a/httpclient5-testing/pom.xml b/httpclient5-testing/pom.xml
index 863f8bad4..63377a25a 100644
--- a/httpclient5-testing/pom.xml
+++ b/httpclient5-testing/pom.xml
@@ -97,8 +97,43 @@
       <artifactId>rxjava</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
+  <profiles>
+    <profile>
+      <id>docker</id>
+      <build>
+        <plugins>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>${hc.surefire.version}</version>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
   <reporting>
     <plugins>
       <plugin>
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/CachingHttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/CachingHttpAsyncClientCompatibilityTest.java
similarity index 99%
rename from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/CachingHttpAsyncClientCompatibilityTest.java
rename to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/CachingHttpAsyncClientCompatibilityTest.java
index 683286dfd..50fe18ca3 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/CachingHttpAsyncClientCompatibilityTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/CachingHttpAsyncClientCompatibilityTest.java
@@ -24,7 +24,7 @@
  * <http://www.apache.org/>.
  *
  */
-package org.apache.hc.client5.testing.external;
+package org.apache.hc.client5.testing.compatibility;
 
 import java.util.Objects;
 import java.util.concurrent.ExecutionException;
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/CachingHttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/CachingHttpClientCompatibilityTest.java
similarity index 99%
rename from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/CachingHttpClientCompatibilityTest.java
rename to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/CachingHttpClientCompatibilityTest.java
index c24a4c1fc..2526d07c2 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/CachingHttpClientCompatibilityTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/CachingHttpClientCompatibilityTest.java
@@ -24,7 +24,7 @@
  * <http://www.apache.org/>.
  *
  */
-package org.apache.hc.client5.testing.external;
+package org.apache.hc.client5.testing.compatibility;
 
 import java.util.Objects;
 
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java
new file mode 100644
index 000000000..1caef4880
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.testing.compatibility;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+
+import org.apache.hc.client5.http.utils.ByteArrayBuilder;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.images.builder.Transferable;
+
+public final class ContainerImages {
+
+    public static int HTTP_PORT = 8080;
+    public static int HTTPS_PORT = 8443;
+    public static int PROXY_PORT = 8888;
+    public static int PROXY_PW_PROTECTED_PORT = 8889;
+
+    static final byte[] BYTES = "0123456789ABCDEF".getBytes(StandardCharsets.UTF_8);
+
+    static byte[] randomData(final int max) {
+        final Random random = new Random();
+        random.setSeed(System.currentTimeMillis());
+        final int n = random.nextInt(max);
+        final ByteArrayBuilder builder = new ByteArrayBuilder();
+        for (int i = 0; i < n; i++) {
+            builder.append(BYTES);
+        }
+        return builder.toByteArray();
+    }
+
+    public static GenericContainer<?> apacheHttpD() {
+        return new GenericContainer<>(new ImageFromDockerfile()
+                .withFileFromClasspath("server-cert.pem", "docker/httpd/server-cert.pem")
+                .withFileFromClasspath("server-key.pem", "docker/httpd/server-key.pem")
+                .withFileFromClasspath("httpd.conf", "docker/httpd/httpd.conf")
+                .withFileFromClasspath("httpd-ssl.conf", "docker/httpd/httpd-ssl.conf")
+                .withFileFromTransferable("111", Transferable.of(randomData(102400)))
+                .withFileFromTransferable("222", Transferable.of(randomData(102400)))
+                .withFileFromTransferable("333", Transferable.of(randomData(102400)))
+                .withDockerfileFromBuilder(builder ->
+                        builder
+                                .from("httpd:2.4")
+                                .env("httpd_home", "/usr/local/apache2")
+                                .env("var_dir", "/var/httpd")
+                                .env("www_dir", "${var_dir}/www")
+                                .env("private_dir", "${www_dir}/private")
+                                .run("mkdir ${httpd_home}/ssl")
+                                .copy("server-cert.pem", "${httpd_home}/ssl/")
+                                .copy("server-key.pem", "${httpd_home}/ssl/")
+                                .copy("httpd.conf", "${httpd_home}/conf/")
+                                .copy("httpd-ssl.conf", "${httpd_home}/conf/extra/")
+                                .copy("111", "${www_dir}/")
+                                .copy("222", "${www_dir}/")
+                                .copy("333", "${www_dir}/")
+                                .run("mkdir -p ${private_dir}")
+                                //# user: testuser; pwd: nopassword
+                                .run("echo \"testuser:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=\" > ${private_dir}/.htpasswd")
+                                .run("echo \"testuser:Restricted Files:73deccd22e07066db8c405e5364335f5\" > ${private_dir}/.htpasswd_digest")
+                                .run("echo \"Big Secret\" > ${private_dir}/big-secret.txt")
+                                .build()))
+                .withExposedPorts(HTTP_PORT, HTTPS_PORT);
+    }
+
+    public static GenericContainer<?> squid() {
+        return new GenericContainer<>(new ImageFromDockerfile()
+                .withFileFromClasspath("squid.conf", "docker/squid/squid.conf")
+                .withDockerfileFromBuilder(builder ->
+                        builder
+                                .from("sameersbn/squid:3.3.8-22")
+                                .env("conf_dir", "/etc/squid3")
+                                .copy("squid.conf", "${conf_dir}/")
+                                .run("echo \"squid:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=\" > ${conf_dir}/htpasswd")
+                                .build()))
+                .withExposedPorts(PROXY_PORT, PROXY_PW_PROTECTED_PORT);
+
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/HttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpAsyncClientCompatibilityTest.java
similarity index 99%
rename from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/HttpAsyncClientCompatibilityTest.java
rename to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpAsyncClientCompatibilityTest.java
index acc3d7fc5..d044c4826 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/HttpAsyncClientCompatibilityTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpAsyncClientCompatibilityTest.java
@@ -24,7 +24,7 @@
  * <http://www.apache.org/>.
  *
  */
-package org.apache.hc.client5.testing.external;
+package org.apache.hc.client5.testing.compatibility;
 
 import java.util.Objects;
 import java.util.concurrent.ExecutionException;
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpClientCompatibilityIT.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpClientCompatibilityIT.java
new file mode 100644
index 000000000..a67d98ad1
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpClientCompatibilityIT.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.testing.compatibility;
+
+import org.apache.hc.core5.http.URIScheme;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+//         final HttpClientCompatibilityTest[] tests = new HttpClientCompatibilityTest[] {
+//                new HttpClientCompatibilityTest(
+//                        new HttpHost("http", "test-httpd", 8080), new HttpHost("localhost", 8888), null),
+//                new HttpClientCompatibilityTest(
+//                        new HttpHost("http", "test-httpd", 8080), new HttpHost("localhost", 8889),
+//                        new UsernamePasswordCredentials("squid", "nopassword".toCharArray())),
+//                new HttpClientCompatibilityTest(
+//                        new HttpHost("https", "test-httpd", 8443), new HttpHost("localhost", 8888), null),
+//                new HttpClientCompatibilityTest(
+//                        new HttpHost("https", "test-httpd", 8443), new HttpHost("localhost", 8889),
+//                        new UsernamePasswordCredentials("squid", "nopassword".toCharArray()))
+//        };
+
+@Testcontainers(disabledWithoutDocker = true)
+class HttpClientCompatibilityIT {
+
+    @Container
+    static final GenericContainer<?> HTTPD_CONTAINER = ContainerImages.apacheHttpD();
+    @Container
+    static final GenericContainer<?> SQUID = ContainerImages.squid();
+
+    @AfterAll
+    static void cleanup() {
+        SQUID.close();
+        HTTPD_CONTAINER.close();
+    }
+
+    @Nested
+    @DisplayName("Direct HTTP connection ")
+    class DirectOverHttp extends HttpClientCompatibilityTest {
+
+        public DirectOverHttp()throws Exception {
+            super(HTTPD_CONTAINER, URIScheme.HTTP);
+        }
+
+    }
+
+    @Nested
+    @DisplayName("Direct HTTPS connection ")
+    class DirectOverHttps extends HttpClientCompatibilityTest {
+
+        public DirectOverHttps()throws Exception {
+            super(HTTPD_CONTAINER, URIScheme.HTTPS);
+        }
+
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpClientCompatibilityTest.java
new file mode 100644
index 000000000..51d7065ce
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/HttpClientCompatibilityTest.java
@@ -0,0 +1,194 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.testing.compatibility;
+
+import org.apache.hc.client5.http.ContextBuilder;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.CredentialsStore;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpOptions;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.extension.sync.HttpClientResource;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.containers.GenericContainer;
+
+abstract class HttpClientCompatibilityTest {
+
+    @RegisterExtension
+    private final HttpClientResource clientResource;
+
+    private final GenericContainer<?> container;
+    private final URIScheme scheme;
+    private final CredentialsStore credentialsProvider;
+
+    HttpClientCompatibilityTest(final GenericContainer<?> container, final URIScheme scheme) throws Exception {
+        this.container = container;
+        this.scheme = scheme;
+        this.clientResource = new HttpClientResource();
+        this.credentialsProvider = new BasicCredentialsProvider();
+    }
+
+    HttpHost targetHost() {
+        return new HttpHost(scheme.id,
+                container.getHost(),
+                container.getMappedPort(
+                        scheme == URIScheme.HTTPS ? ContainerImages.HTTPS_PORT : ContainerImages.HTTP_PORT));
+    }
+
+    HttpClientContext context() {
+        return ContextBuilder.create()
+                .useCredentialsProvider(credentialsProvider)
+                .build();
+    }
+
+    @Test
+    void test_options_ping() throws Exception {
+        final HttpHost target = targetHost();
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+        final HttpOptions options = new HttpOptions("*");
+        try (ClassicHttpResponse response = client.executeOpen(target, options, context)) {
+            Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+            EntityUtils.consume(response.getEntity());
+        }
+    }
+
+    @Test
+    void test_get() throws Exception {
+        final HttpHost target = targetHost();
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+        final String[] requestUris = new String[] { "/111", "/222", "/333" };
+        for (final String requestUri: requestUris) {
+            final ClassicHttpRequest request = ClassicRequestBuilder.get(requestUri)
+                    .build();
+            try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
+                Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+                EntityUtils.consume(response.getEntity());
+            }
+        }
+    }
+
+    @Test
+    void test_get_connection_close() throws Exception {
+        final HttpHost target = targetHost();
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+        final String[] requestUris = new String[] { "/111", "/222", "/333" };
+        for (final String requestUri: requestUris) {
+            final ClassicHttpRequest request = ClassicRequestBuilder.get(requestUri)
+                    .addHeader(HttpHeaders.CONNECTION, "close")
+                    .build();
+            try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
+                Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+                EntityUtils.consume(response.getEntity());
+            }
+        }
+    }
+
+    @Test
+    void test_wrong_target_auth_scope() throws Exception {
+        final HttpHost target = targetHost();
+        credentialsProvider.setCredentials(
+                new AuthScope("http", "otherhost", -1, "Restricted Files", null),
+                new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+
+        final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt");
+        try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
+            Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+            EntityUtils.consume(response.getEntity());
+        }
+    }
+
+    @Test
+    void test_wrong_target_credentials() throws Exception {
+        final HttpHost target = targetHost();
+        credentialsProvider.setCredentials(
+                new AuthScope(target),
+                new UsernamePasswordCredentials("testuser", "wrong password".toCharArray()));
+
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+
+        final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt");
+        try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
+            Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+            EntityUtils.consume(response.getEntity());
+        }
+    }
+
+    @Test
+    void test_correct_target_credentials() throws Exception {
+        final HttpHost target = targetHost();
+        credentialsProvider.setCredentials(
+                new AuthScope(target),
+                new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+
+        final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt");
+        try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
+            Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+            EntityUtils.consume(response.getEntity());
+        }
+    }
+
+    @Test
+    void test_correct_target_credentials_no_keep_alive() throws Exception {
+        final HttpHost target = targetHost();
+        credentialsProvider.setCredentials(
+                new AuthScope(target),
+                new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+        final CloseableHttpClient client = clientResource.client();
+        final HttpClientContext context = context();
+
+        final ClassicHttpRequest request = ClassicRequestBuilder.get("/private/big-secret.txt")
+                .addHeader(HttpHeaders.CONNECTION, "close")
+                .build();
+        try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
+            Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+            EntityUtils.consume(response.getEntity());
+        }
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/Result.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/Result.java
new file mode 100644
index 000000000..73794c96a
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/Result.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.testing.compatibility;
+
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.RequestLine;
+import org.apache.hc.core5.http.message.StatusLine;
+
+public final class Result<T> {
+
+    public final HttpRequest request;
+    public final HttpResponse response;
+    public final T content;
+    public final Exception exception;
+
+    public enum Status { OK, NOK }
+
+    public Result(final HttpRequest request, final Exception exception) {
+        this.request = request;
+        this.response = null;
+        this.content = null;
+        this.exception = exception;
+    }
+
+    public Result(final HttpRequest request, final HttpResponse response, final T content) {
+        this.request = request;
+        this.response = response;
+        this.content = content;
+        this.exception = null;
+    }
+
+    public Status getStatus() {
+        return exception != null ? Status.NOK : Status.OK;
+    }
+
+    public boolean isOK() {
+        return exception == null;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder buf = new StringBuilder();
+        buf.append(new RequestLine(request));
+        buf.append(" -> ");
+        if (exception != null) {
+            buf.append("NOK: ").append(exception);
+        } else {
+            if (response != null) {
+                buf.append("OK: ").append(new StatusLine(response));
+            }
+        }
+        return buf.toString();
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/HttpClientResource.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/HttpClientResource.java
new file mode 100644
index 000000000..40577fd37
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/HttpClientResource.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.testing.extension.sync;
+
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.function.Consumer;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+public class HttpClientResource implements AfterEachCallback {
+
+    private final HttpClientBuilder clientBuilder;
+    private CloseableHttpClient client;
+
+    public HttpClientResource() throws IOException {
+        try {
+            this.clientBuilder = HttpClients.custom()
+                    .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
+                            .setTlsSocketStrategy(new DefaultClientTlsStrategy(SSLContexts.custom()
+                                    .loadTrustMaterial(getClass().getResource("/test-ca.keystore"), "nopassword".toCharArray())
+                                    .build()))
+                            .build());
+        } catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    public void configure(final Consumer<HttpClientBuilder> customizer) {
+        customizer.accept(clientBuilder);
+    }
+
+    @Override
+    public void afterEach(final ExtensionContext extensionContext) {
+        if (client != null) {
+            client.close(CloseMode.GRACEFUL);
+        }
+    }
+
+    public CloseableHttpClient client() {
+        if (client == null) {
+            client = clientBuilder.build();
+        }
+        return client;
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientResources.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientResources.java
index c38c7fba1..2dcb6356e 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientResources.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientResources.java
@@ -29,7 +29,6 @@ package org.apache.hc.client5.testing.extension.sync;
 
 import java.util.function.Consumer;
 
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
 import org.apache.hc.core5.http.URIScheme;
 import org.apache.hc.core5.io.CloseMode;
 import org.apache.hc.core5.util.Asserts;
@@ -50,7 +49,6 @@ public class TestClientResources implements AfterEachCallback {
     private final TestClientBuilder clientBuilder;
 
     private TestServer server;
-    private PoolingHttpClientConnectionManager connManager;
     private TestClient client;
 
     public TestClientResources(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final Timeout timeout) {
@@ -76,11 +74,6 @@ public class TestClientResources implements AfterEachCallback {
         if (client != null) {
             client.close(CloseMode.GRACEFUL);
         }
-
-        if (connManager != null) {
-            connManager.close(CloseMode.IMMEDIATE);
-        }
-
         if (server != null) {
             server.shutdown(CloseMode.IMMEDIATE);
         }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/HttpClientCompatibilityTest.java
deleted file mode 100644
index 0b73e9997..000000000
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/external/HttpClientCompatibilityTest.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.testing.external;
-
-import java.util.Objects;
-
-import javax.net.ssl.SSLContext;
-
-import org.apache.hc.client5.http.auth.AuthScope;
-import org.apache.hc.client5.http.auth.Credentials;
-import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
-import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.classic.methods.HttpOptions;
-import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
-import org.apache.hc.client5.http.impl.classic.HttpClients;
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
-import org.apache.hc.core5.http.ClassicHttpResponse;
-import org.apache.hc.core5.http.HeaderElements;
-import org.apache.hc.core5.http.HttpHeaders;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.io.entity.EntityUtils;
-import org.apache.hc.core5.ssl.SSLContexts;
-import org.apache.hc.core5.util.TextUtils;
-import org.apache.hc.core5.util.TimeValue;
-
-public class HttpClientCompatibilityTest {
-
-    public static void main(final String... args) throws Exception {
-        final HttpClientCompatibilityTest[] tests = new HttpClientCompatibilityTest[] {
-                new HttpClientCompatibilityTest(
-                        new HttpHost("http", "localhost", 8080), null, null),
-                new HttpClientCompatibilityTest(
-                        new HttpHost("http", "test-httpd", 8080), new HttpHost("localhost", 8888), null),
-                new HttpClientCompatibilityTest(
-                        new HttpHost("http", "test-httpd", 8080), new HttpHost("localhost", 8889),
-                        new UsernamePasswordCredentials("squid", "nopassword".toCharArray())),
-                new HttpClientCompatibilityTest(
-                        new HttpHost("https", "localhost", 8443), null, null),
-                new HttpClientCompatibilityTest(
-                        new HttpHost("https", "test-httpd", 8443), new HttpHost("localhost", 8888), null),
-                new HttpClientCompatibilityTest(
-                        new HttpHost("https", "test-httpd", 8443), new HttpHost("localhost", 8889),
-                        new UsernamePasswordCredentials("squid", "nopassword".toCharArray()))
-        };
-        for (final HttpClientCompatibilityTest test: tests) {
-            try {
-                test.execute();
-            } finally {
-                test.shutdown();
-            }
-        }
-    }
-
-    private final HttpHost target;
-    private final HttpHost proxy;
-    private final BasicCredentialsProvider credentialsProvider;
-    private final PoolingHttpClientConnectionManager connManager;
-    private final CloseableHttpClient client;
-
-    HttpClientCompatibilityTest(
-            final HttpHost target,
-            final HttpHost proxy,
-            final Credentials proxyCreds) throws Exception {
-        this.target = target;
-        this.proxy = proxy;
-        this.credentialsProvider = new BasicCredentialsProvider();
-        final RequestConfig requestConfig = RequestConfig.DEFAULT;
-        if (proxy != null && proxyCreds != null) {
-            this.credentialsProvider.setCredentials(new AuthScope(proxy), proxyCreds);
-        }
-        final SSLContext sslContext = SSLContexts.custom()
-                .loadTrustMaterial(getClass().getResource("/test-ca.keystore"), "nopassword".toCharArray()).build();
-        this.connManager = PoolingHttpClientConnectionManagerBuilder.create()
-                .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslContext))
-                .build();
-        this.client = HttpClients.custom()
-                .setConnectionManager(this.connManager)
-                .setProxy(this.proxy)
-                .setDefaultRequestConfig(requestConfig)
-                .build();
-    }
-
-    void shutdown() throws Exception {
-        client.close();
-    }
-
-    enum TestResult { OK, NOK }
-
-    private void logResult(final TestResult result, final HttpRequest request, final String message) {
-        final StringBuilder buf = new StringBuilder();
-        buf.append(result);
-        if (buf.length() == 2) {
-            buf.append(" ");
-        }
-        buf.append(": ").append(target);
-        if (proxy != null) {
-            buf.append(" via ").append(proxy);
-        }
-        buf.append(": ");
-        buf.append(request.getMethod()).append(" ").append(request.getRequestUri());
-        if (message != null && !TextUtils.isBlank(message)) {
-            buf.append(" -> ").append(message);
-        }
-        System.out.println(buf);
-    }
-
-    void execute() {
-
-        // Initial ping
-        {
-            final HttpClientContext context = HttpClientContext.create();
-            context.setCredentialsProvider(credentialsProvider);
-            final HttpOptions options = new HttpOptions("*");
-            try (ClassicHttpResponse response = client.executeOpen(target, options, context)) {
-                final int code = response.getCode();
-                EntityUtils.consume(response.getEntity());
-                if (code == HttpStatus.SC_OK) {
-                    logResult(TestResult.OK, options, Objects.toString(response.getFirstHeader("server")));
-                } else {
-                    logResult(TestResult.NOK, options, "(status " + code + ")");
-                }
-            } catch (final Exception ex) {
-                logResult(TestResult.NOK, options, "(" + ex.getMessage() + ")");
-            }
-        }
-        // Basic GET requests
-        {
-            connManager.closeIdle(TimeValue.NEG_ONE_MILLISECOND);
-            final HttpClientContext context = HttpClientContext.create();
-            context.setCredentialsProvider(credentialsProvider);
-            final String[] requestUris = new String[] {"/", "/news.html", "/status.html"};
-            for (final String requestUri: requestUris) {
-                final HttpGet httpGet = new HttpGet(requestUri);
-                try (ClassicHttpResponse response = client.executeOpen(target, httpGet, context)) {
-                    final int code = response.getCode();
-                    EntityUtils.consume(response.getEntity());
-                    if (code == HttpStatus.SC_OK) {
-                        logResult(TestResult.OK, httpGet, "200");
-                    } else {
-                        logResult(TestResult.NOK, httpGet, "(status " + code + ")");
-                    }
-                } catch (final Exception ex) {
-                    logResult(TestResult.NOK, httpGet, "(" + ex.getMessage() + ")");
-                }
-            }
-        }
-        // Wrong target auth scope
-        {
-            connManager.closeIdle(TimeValue.NEG_ONE_MILLISECOND);
-            credentialsProvider.setCredentials(
-                    new AuthScope("http", "otherhost", -1, "Restricted Files", null),
-                    new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
-            final HttpClientContext context = HttpClientContext.create();
-            context.setCredentialsProvider(credentialsProvider);
-
-            final HttpGet httpGetSecret = new HttpGet("/private/big-secret.txt");
-            try (ClassicHttpResponse response = client.executeOpen(target, httpGetSecret, context)) {
-                final int code = response.getCode();
-                EntityUtils.consume(response.getEntity());
-                if (code == HttpStatus.SC_UNAUTHORIZED) {
-                    logResult(TestResult.OK, httpGetSecret, "401 (wrong target auth scope)");
-                } else {
-                    logResult(TestResult.NOK, httpGetSecret, "(status " + code + ")");
-                }
-            } catch (final Exception ex) {
-                logResult(TestResult.NOK, httpGetSecret, "(" + ex.getMessage() + ")");
-            }
-        }
-        // Wrong target credentials
-        {
-            connManager.closeIdle(TimeValue.NEG_ONE_MILLISECOND);
-            credentialsProvider.setCredentials(
-                    new AuthScope(target),
-                    new UsernamePasswordCredentials("testuser", "wrong password".toCharArray()));
-            final HttpClientContext context = HttpClientContext.create();
-            context.setCredentialsProvider(credentialsProvider);
-
-            final HttpGet httpGetSecret = new HttpGet("/private/big-secret.txt");
-            try (ClassicHttpResponse response = client.executeOpen(target, httpGetSecret, context)) {
-                final int code = response.getCode();
-                EntityUtils.consume(response.getEntity());
-                if (code == HttpStatus.SC_UNAUTHORIZED) {
-                    logResult(TestResult.OK, httpGetSecret, "401 (wrong target creds)");
-                } else {
-                    logResult(TestResult.NOK, httpGetSecret, "(status " + code + ")");
-                }
-            } catch (final Exception ex) {
-                logResult(TestResult.NOK, httpGetSecret, "(" + ex.getMessage() + ")");
-            }
-        }
-        // Correct target credentials
-        {
-            connManager.closeIdle(TimeValue.NEG_ONE_MILLISECOND);
-            credentialsProvider.setCredentials(
-                    new AuthScope(target),
-                    new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
-            final HttpClientContext context = HttpClientContext.create();
-            context.setCredentialsProvider(credentialsProvider);
-
-            final HttpGet httpGetSecret = new HttpGet("/private/big-secret.txt");
-            try (ClassicHttpResponse response = client.executeOpen(target, httpGetSecret, context)) {
-                final int code = response.getCode();
-                EntityUtils.consume(response.getEntity());
-                if (code == HttpStatus.SC_OK) {
-                    logResult(TestResult.OK, httpGetSecret, "200 (correct target creds)");
-                } else {
-                    logResult(TestResult.NOK, httpGetSecret, "(status " + code + ")");
-                }
-            } catch (final Exception ex) {
-                logResult(TestResult.NOK, httpGetSecret, "(" + ex.getMessage() + ")");
-            }
-        }
-        // Correct target credentials (no keep-alive)
-        {
-            connManager.closeIdle(TimeValue.NEG_ONE_MILLISECOND);
-            credentialsProvider.setCredentials(
-                    new AuthScope(target),
-                    new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
-            final HttpClientContext context = HttpClientContext.create();
-            context.setCredentialsProvider(credentialsProvider);
-
-            final HttpGet httpGetSecret = new HttpGet("/private/big-secret.txt");
-            httpGetSecret.setHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE);
-            try (ClassicHttpResponse response = client.executeOpen(target, httpGetSecret, context)) {
-                final int code = response.getCode();
-                EntityUtils.consume(response.getEntity());
-                if (code == HttpStatus.SC_OK) {
-                    logResult(TestResult.OK, httpGetSecret, "200 (correct target creds / no keep-alive)");
-                } else {
-                    logResult(TestResult.NOK, httpGetSecret, "(status " + code + ")");
-                }
-            } catch (final Exception ex) {
-                logResult(TestResult.NOK, httpGetSecret, "(" + ex.getMessage() + ")");
-            }
-        }
-    }
-
-}
diff --git a/httpclient5-testing/docker/BUILDING.txt b/httpclient5-testing/src/test/resources/docker/httpd/BUILDING.txt
similarity index 56%
rename from httpclient5-testing/docker/BUILDING.txt
rename to httpclient5-testing/src/test/resources/docker/httpd/BUILDING.txt
index 419a61d2e..fdc7d9056 100644
--- a/httpclient5-testing/docker/BUILDING.txt
+++ b/httpclient5-testing/src/test/resources/docker/httpd/BUILDING.txt
@@ -1,26 +1,3 @@
-Building Docker containers for compatibility tests
-========================================================
-
-= Apache HTTPD 2.4 image
-
-Remark: omit sudo command if executing as root
----
-sudo docker build -t httpclient-tests-httpd apache-httpd
----
-
-= Squid 3.3 image
-
-Remark: omit sudo command if executing as root
----
-sudo docker build -t httpclient-tests-squid squid
----
-
-= Start containers
-
----
-sudo docker-compose up
----
-
 = SSL key / cert material (optional)
 
 # Issue a certificate request
diff --git a/httpclient5-testing/docker/apache-httpd/httpd-ssl.conf b/httpclient5-testing/src/test/resources/docker/httpd/httpd-ssl.conf
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/httpd-ssl.conf
rename to httpclient5-testing/src/test/resources/docker/httpd/httpd-ssl.conf
diff --git a/httpclient5-testing/docker/apache-httpd/httpd.conf b/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/httpd.conf
rename to httpclient5-testing/src/test/resources/docker/httpd/httpd.conf
diff --git a/httpclient5-testing/docker/apache-httpd/index.txt b/httpclient5-testing/src/test/resources/docker/httpd/index.txt
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/index.txt
rename to httpclient5-testing/src/test/resources/docker/httpd/index.txt
diff --git a/httpclient5-testing/docker/apache-httpd/openssl.cnf b/httpclient5-testing/src/test/resources/docker/httpd/openssl.cnf
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/openssl.cnf
rename to httpclient5-testing/src/test/resources/docker/httpd/openssl.cnf
diff --git a/httpclient5-testing/docker/apache-httpd/serial b/httpclient5-testing/src/test/resources/docker/httpd/serial
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/serial
rename to httpclient5-testing/src/test/resources/docker/httpd/serial
diff --git a/httpclient5-testing/docker/apache-httpd/server-cert.pem b/httpclient5-testing/src/test/resources/docker/httpd/server-cert.pem
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/server-cert.pem
rename to httpclient5-testing/src/test/resources/docker/httpd/server-cert.pem
diff --git a/httpclient5-testing/docker/apache-httpd/server-key.pem b/httpclient5-testing/src/test/resources/docker/httpd/server-key.pem
similarity index 100%
rename from httpclient5-testing/docker/apache-httpd/server-key.pem
rename to httpclient5-testing/src/test/resources/docker/httpd/server-key.pem
diff --git a/httpclient5-testing/docker/squid/squid.conf b/httpclient5-testing/src/test/resources/docker/squid/squid.conf
similarity index 100%
rename from httpclient5-testing/docker/squid/squid.conf
rename to httpclient5-testing/src/test/resources/docker/squid/squid.conf
diff --git a/pom.xml b/pom.xml
index d70eca288..5b0bff4b9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,9 +74,9 @@
     <mockito.version>4.11.0</mockito.version>
     <hc.stylecheck.version>1</hc.stylecheck.version>
     <rxjava.version>2.2.21</rxjava.version>
+    <testcontainers.version>1.20.2</testcontainers.version>
     <api.comparison.version>5.3</api.comparison.version>
     <hc.animal-sniffer.signature.ignores>javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer</hc.animal-sniffer.signature.ignores>
-    <japicmp.version>0.15.4</japicmp.version>
   </properties>
 
   <dependencyManagement>
@@ -182,6 +182,16 @@
         <version>${hamcrest.version}</version>
         <scope>test</scope>
       </dependency>
+      <dependency>
+        <groupId>org.testcontainers</groupId>
+        <artifactId>testcontainers</artifactId>
+        <version>${testcontainers.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.testcontainers</groupId>
+        <artifactId>junit-jupiter</artifactId>
+        <version>${testcontainers.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>