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/06/21 09:18:13 UTC

(httpcomponents-client) 01/03: Redesign of test server APIs and integration test setup

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

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

commit 034cd65aa52f8cba5172c51116638c72f56d3aa8
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Wed Jun 19 17:06:45 2024 +0200

    Redesign of test server APIs and integration test setup
---
 .../AbstractHttpAsyncClientAuthenticationTest.java | 171 ++++-----
 .../async/AbstractHttpAsyncFundamentalsTest.java   |  59 ++-
 .../async/AbstractHttpAsyncRedirectsTest.java      | 418 ++++++++++----------
 .../AbstractHttpReactiveFundamentalsTest.java      |  75 ++--
 .../testing/async/AbstractIntegrationTestBase.java |  84 ++--
 .../testing/async/HttpIntegrationTests.java        |  80 ++--
 .../hc/client5/testing/async/TestH2Async.java      |  19 +-
 .../client5/testing/async/TestH2AsyncMinimal.java  |  19 +-
 .../client5/testing/async/TestH2AsyncRedirect.java |  22 +-
 .../testing/async/TestH2ClientAuthentication.java  |  65 +---
 .../hc/client5/testing/async/TestH2Reactive.java   |  19 +-
 .../testing/async/TestH2ReactiveMinimal.java       |  20 +-
 .../hc/client5/testing/async/TestHttp1Async.java   |  48 +--
 .../testing/async/TestHttp1AsyncMinimal.java       |  29 +-
 .../testing/async/TestHttp1AsyncRedirects.java     |  69 ++--
 .../TestHttp1AsyncStatefulConnManagement.java      |  38 +-
 .../async/TestHttp1ClientAuthentication.java       |  90 +----
 .../client5/testing/async/TestHttp1Reactive.java   |  43 +--
 .../testing/async/TestHttp1ReactiveMinimal.java    |  31 +-
 .../testing/async/TestHttp1RequestReExecution.java |  32 +-
 .../async/TestHttpAsyncMinimalTlsHandshake.java    |  43 +--
 .../testing/async/TestHttpAsyncProtocolPolicy.java |  35 +-
 .../ClientProtocolLevel.java}                      |  24 +-
 .../extension/H2OnlyMinimalTestClientBuilder.java  |  82 ++++
 .../async/extension/H2OnlyTestClientBuilder.java   | 136 +++++++
 .../async/extension/MinimalTestClientBuilder.java  | 111 ++++++
 .../ServerProtocolLevel.java}                      |  24 +-
 .../async/extension/StandardTestClientBuilder.java | 174 +++++++++
 .../testing/async/extension/TestAsyncClient.java   | 117 ++++++
 .../async/extension/TestAsyncClientBuilder.java    | 106 ++++++
 .../async/extension/TestAsyncResources.java        | 211 +++-------
 .../testing/async/extension/TestAsyncServer.java   | 101 +++++
 .../async/extension/TestAsyncServerBootstrap.java  | 133 +++++++
 .../hc/client5/testing/fluent/TestFluent.java      |  95 ++---
 .../testing/sync/AbstractIntegrationTestBase.java  |  81 ++++
 .../testing/sync/TestBasicConnectionManager.java   |  40 +-
 .../testing/sync/TestClientAuthentication.java     | 294 +++++++-------
 .../testing/sync/TestClientRequestExecution.java   |  89 ++---
 .../testing/sync/TestConnectionManagement.java     |  75 ++--
 .../client5/testing/sync/TestConnectionReuse.java  | 126 +++---
 .../client5/testing/sync/TestContentCodings.java   | 135 +++----
 .../testing/sync/TestCookieVirtualHost.java        | 102 +++--
 .../testing/sync/TestIdleConnectionEviction.java   |  25 +-
 .../sync/TestMinimalClientRequestExecution.java    |  33 +-
 .../hc/client5/testing/sync/TestRedirects.java     | 424 ++++++++++-----------
 .../testing/sync/TestStatefulConnManagement.java   |  56 ++-
 .../extension/ClientProtocolLevel.java}            |  24 +-
 .../sync/extension/MinimalTestClientBuilder.java   |  83 ++++
 .../sync/extension/StandardTestClientBuilder.java  | 159 ++++++++
 .../client5/testing/sync/extension/TestClient.java |  77 ++++
 .../testing/sync/extension/TestClientBuilder.java  |  94 +++++
 .../sync/extension/TestClientResources.java        | 129 ++-----
 .../client5/testing/sync/extension/TestServer.java |  67 ++++
 .../sync/extension/TestServerBootstrap.java        | 123 ++++++
 .../http/impl/classic/CloseableHttpResponse.java   |   7 +-
 55 files changed, 3005 insertions(+), 2061 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java
index f9867fde5..75a27f582 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java
@@ -36,10 +36,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
-import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
@@ -52,73 +50,44 @@ import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.BasicTestAuthenticator;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.client5.testing.auth.Authenticator;
 import org.apache.hc.client5.testing.auth.BearerAuthenticationHandler;
-import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequestInterceptor;
 import org.apache.hc.core5.http.HttpResponse;
-import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Lookup;
 import org.apache.hc.core5.http.config.Registry;
 import org.apache.hc.core5.http.config.RegistryBuilder;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
 import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.net.URIAuthority;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
-public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends CloseableHttpAsyncClient> extends AbstractIntegrationTestBase {
+public abstract class AbstractHttpAsyncClientAuthenticationTest extends AbstractIntegrationTestBase {
 
-    public AbstractHttpAsyncClientAuthenticationTest(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    abstract protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception;
-
-    protected H2TestServer startServer() throws Exception {
-        return startServer(requestHandler -> new AuthenticatingAsyncDecorator(requestHandler, new BasicTestAuthenticator("test:test", "test realm")));
-    }
-
-    interface TestClientBuilder {
-
-        TestClientBuilder setDefaultAuthSchemeRegistry(Lookup<AuthSchemeFactory> authSchemeRegistry);
-
-        TestClientBuilder setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy);
-
-        TestClientBuilder addResponseInterceptor(HttpResponseInterceptor responseInterceptor);
-
-        TestClientBuilder addRequestInterceptor(HttpRequestInterceptor requestInterceptor);
-
-    }
-
-    abstract protected T startClientCustom(final Consumer<TestClientBuilder> clientCustomizer) throws Exception;
-
-    T startClient() throws Exception {
-        return startClientCustom(c -> {});
+    public AbstractHttpAsyncClientAuthenticationTest(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
+        super(scheme, clientProtocolLevel, serverProtocolLevel);
     }
 
     @Test
     public void testBasicAuthenticationNoCreds() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         final HttpClientContext context = HttpClientContext.create();
@@ -138,11 +107,10 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationFailure() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -164,11 +132,10 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationSuccess() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -191,11 +158,10 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationWithEntitySuccess() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -218,11 +184,10 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationExpectationFailure() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -244,11 +209,10 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationExpectationSuccess() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -272,12 +236,12 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationCredentialsCaching() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
-        final T client = startClientCustom(builder ->  builder.setTargetAuthenticationStrategy(authStrategy));
+        configureClient(builder ->  builder.setTargetAuthenticationStrategy(authStrategy));
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         context.setCredentialsProvider(CredentialsProviderBuilder.create()
@@ -299,17 +263,17 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
         final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
 
-        final T client = startClientCustom(builder -> builder
+        configureClient(builder -> builder
                 .setTargetAuthenticationStrategy(authStrategy)
-                .addResponseInterceptor((response, entity, context)
+                .addResponseInterceptorFirst((response, entity, context)
                         -> responseQueue.add(BasicResponseBuilder.copy(response).build())));
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
                 .add(target, "test", "test".toCharArray())
@@ -364,11 +328,10 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testAuthenticationUserinfoInRequestFailure() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
@@ -396,17 +359,18 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
             }
         };
 
-        final H2TestServer server = startServer(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, authenticator) {
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap
+                .register("*", AsyncEchoHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, authenticator) {
 
-            @Override
-            protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
-                unauthorized.removeHeaders(HttpHeaders.WWW_AUTHENTICATE);
-                unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, "MyBasic realm=\"test realm\"");
-            }
+                    @Override
+                    protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
+                        unauthorized.removeHeaders(HttpHeaders.WWW_AUTHENTICATE);
+                        unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, "MyBasic realm=\"test realm\"");
+                    }
 
-        });
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+                }));
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -425,7 +389,9 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
                 })
                 .build();
 
-        final T client = startClientCustom(builder -> builder.setDefaultAuthSchemeRegistry(authSchemeRegistry));
+        configureClient(builder -> builder.setDefaultAuthSchemeRegistry(authSchemeRegistry));
+        final TestAsyncClient client = startClient();
+
 
         final RequestConfig config = RequestConfig.custom()
                 .setTargetPreferredAuthSchemes(Collections.singletonList("MyBasic"))
@@ -448,18 +414,19 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
 
     @Test
     public void testAuthenticationFallback() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, new BasicTestAuthenticator("test:test", "test realm")) {
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap
+                .register("*", AsyncEchoHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, new BasicTestAuthenticator("test:test", "test realm")) {
 
-            @Override
-            protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
-                unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"test realm\" invalid");
-            }
+                    @Override
+                    protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
+                        unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"test realm\" invalid");
+                    }
 
-        });
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+                }));
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -489,15 +456,17 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
             buf.append(CHARS.charAt(secureRandom.nextInt(CHARS.length() - 1)));
         }
         final String token = buf.toString();
-        final H2TestServer server = startServer(requestHandler ->
-                new AuthenticatingAsyncDecorator(
-                        requestHandler,
-                        new BearerAuthenticationHandler(),
-                        new BasicTestAuthenticator(token, "test realm")));
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap
+                .register("*", AsyncEchoHandler::new)
+                .setExchangeHandlerDecorator(requestHandler ->
+                        new AuthenticatingAsyncDecorator(
+                                requestHandler,
+                                new BearerAuthenticationHandler(),
+                                new BasicTestAuthenticator(token, "test realm"))));
+
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         final HttpClientContext context1 = HttpClientContext.create();
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
index 05f818b3e..4b0d40ac9 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
@@ -40,8 +40,10 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.core5.concurrent.FutureCallback;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHost;
@@ -53,26 +55,22 @@ import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers;
 import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer;
 import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
 import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
 
-public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpAsyncClient> extends AbstractIntegrationTestBase {
+public abstract class AbstractHttpAsyncFundamentalsTest extends AbstractIntegrationTestBase {
 
-    protected AbstractHttpAsyncFundamentalsTest(final URIScheme scheme) {
-        super(scheme);
+    protected AbstractHttpAsyncFundamentalsTest(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
+        super(scheme, clientProtocolLevel, serverProtocolLevel);
     }
 
-    abstract protected H2TestServer startServer() throws Exception;
-
-    abstract protected T startClient() throws Exception;
-
     @Test
     public void testSequentialGetRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
+
         for (int i = 0; i < 3; i++) {
             final Future<SimpleHttpResponse> future = client.execute(
                     SimpleRequestBuilder.get()
@@ -90,10 +88,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
     @Test
     public void testSequentialHeadRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
+        final TestAsyncClient client = startClient();
         for (int i = 0; i < 3; i++) {
             final Future<SimpleHttpResponse> future = client.execute(
                     SimpleRequestBuilder.head()
@@ -110,10 +107,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
     @Test
     public void testSequentialPostRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/echo/*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/echo/*", AsyncEchoHandler::new));
+        final HttpHost target = startServer();
+        final TestAsyncClient client = startClient();
         for (int i = 0; i < 3; i++) {
             final byte[] b1 = new byte[1024];
             final Random rnd = new Random(System.currentTimeMillis());
@@ -133,10 +129,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
     @Test
     public void testConcurrentPostRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/echo/*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/echo/*", AsyncEchoHandler::new));
+        final HttpHost target = startServer();
+        final TestAsyncClient client = startClient();
         final byte[] b1 = new byte[1024];
         final Random rnd = new Random(System.currentTimeMillis());
         rnd.nextBytes(b1);
@@ -165,10 +160,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
     @Test
     public void testRequestExecutionFromCallback() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
+        final TestAsyncClient client = startClient();
         final int requestNum = 50;
         final AtomicInteger count = new AtomicInteger(requestNum);
         final Queue<SimpleHttpResponse> resultQueue = new ConcurrentLinkedQueue<>();
@@ -233,10 +227,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
     @Test
     public void testBadRequest() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
+        final TestAsyncClient client = startClient();
         final Future<SimpleHttpResponse> future = client.execute(
                 SimpleRequestBuilder.get()
                         .setHttpHost(target)
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncRedirectsTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncRedirectsTest.java
index 0f9d97356..4d4b0952d 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncRedirectsTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncRedirectsTest.java
@@ -41,13 +41,15 @@ import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.cookie.BasicCookieStore;
 import org.apache.hc.client5.http.cookie.CookieStore;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.OldPathRedirectResolver;
-import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
+import org.apache.hc.client5.testing.async.extension.TestAsyncServer;
+import org.apache.hc.client5.testing.async.extension.TestAsyncServerBootstrap;
 import org.apache.hc.client5.testing.redirect.Redirect;
-import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpException;
@@ -55,48 +57,31 @@ import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
-import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.net.URIBuilder;
-import org.apache.hc.core5.reactor.IOReactorConfig;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.apache.hc.core5.util.TimeValue;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsyncClient> extends AbstractIntegrationTestBase {
+public abstract class AbstractHttpAsyncRedirectsTest extends AbstractIntegrationTestBase {
 
-    private final HttpVersion version;
-
-    public AbstractHttpAsyncRedirectsTest(final URIScheme scheme, final HttpVersion version) {
-        super(scheme);
-        this.version = version;
-    }
-
-    abstract protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception;
-
-    protected H2TestServer startServer() throws Exception {
-        return startServer(null);
+    public AbstractHttpAsyncRedirectsTest(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
+        super(scheme, clientProtocolLevel, serverProtocolLevel);
     }
 
-    abstract protected T startClient() throws Exception;
-
     @Test
     public void testBasicRedirect300() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES)));
-
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -115,13 +100,14 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect301() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -141,13 +127,14 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect302() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -167,19 +154,20 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect302NoLocation() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.startsWith("/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, null);
-                    }
-                    return null;
-                }));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.startsWith("/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, null);
+                            }
+                            return null;
+                        })));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -198,13 +186,15 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect303() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_SEE_OTHER)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_SEE_OTHER))));
 
-        final T client = startClient();
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -224,19 +214,19 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect304() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        server.register("/oldlocation/*", () -> new AbstractSimpleServerExchangeHandler() {
-
-            @Override
-            protected SimpleHttpResponse handle(final SimpleHttpRequest request,
-                                                final HttpCoreContext context) throws HttpException {
-                return SimpleHttpResponse.create(HttpStatus.SC_NOT_MODIFIED, (String) null);
-            }
-        });
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .register("/oldlocation/*", () -> new AbstractSimpleServerExchangeHandler() {
+
+                    @Override
+                    protected SimpleHttpResponse handle(final SimpleHttpRequest request,
+                                                        final HttpCoreContext context) throws HttpException {
+                        return SimpleHttpResponse.create(HttpStatus.SC_NOT_MODIFIED, (String) null);
+                    }
+                }));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -255,19 +245,19 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect305() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        server.register("/oldlocation/*", () -> new AbstractSimpleServerExchangeHandler() {
-
-            @Override
-            protected SimpleHttpResponse handle(final SimpleHttpRequest request,
-                                                final HttpCoreContext context) throws HttpException {
-                return SimpleHttpResponse.create(HttpStatus.SC_USE_PROXY, (String) null);
-            }
-        });
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .register("/oldlocation/*", () -> new AbstractSimpleServerExchangeHandler() {
+
+                    @Override
+                    protected SimpleHttpResponse handle(final SimpleHttpRequest request,
+                                                        final HttpCoreContext context) throws HttpException {
+                        return SimpleHttpResponse.create(HttpStatus.SC_USE_PROXY, (String) null);
+                    }
+                }));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -286,13 +276,14 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testBasicRedirect307() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_TEMPORARY_REDIRECT)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_TEMPORARY_REDIRECT))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -312,14 +303,15 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testMaxRedirectCheck() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
-                        HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
+                                HttpStatus.SC_MOVED_TEMPORARILY))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final RequestConfig config = RequestConfig.custom()
                 .setCircularRedirectsAllowed(true)
@@ -337,14 +329,15 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testCircularRedirect() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
-                        HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
+                                HttpStatus.SC_MOVED_TEMPORARILY))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final RequestConfig config = RequestConfig.custom()
                 .setCircularRedirectsAllowed(false)
@@ -363,14 +356,14 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testPostRedirect() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/echo", HttpStatus.SC_TEMPORARY_REDIRECT)));
-
-        server.register("/echo/*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/echo/*", AsyncEchoHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/echo", HttpStatus.SC_TEMPORARY_REDIRECT))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -391,14 +384,14 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testPostRedirectSeeOther() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/echo", HttpStatus.SC_SEE_OTHER)));
+        configureServer(bootstrap -> bootstrap
+                .register("/echo/*", AsyncEchoHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/echo", HttpStatus.SC_SEE_OTHER))));
+        final HttpHost target = startServer();
 
-        server.register("/echo/*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(
@@ -419,21 +412,21 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testRelativeRedirect() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.startsWith("/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/random/100");
-
-                    }
-                    return null;
-                }));
-
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.startsWith("/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/random/100");
+
+                            }
+                            return null;
+                        })));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
 
@@ -454,21 +447,21 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testRelativeRedirect2() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.equals("/random/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "100");
-
-                    }
-                    return null;
-                }));
-
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.equals("/random/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "100");
+
+                            }
+                            return null;
+                        })));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
 
@@ -489,20 +482,21 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testRejectBogusRedirectLocation() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.equals("/oldlocation/")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "xxx://bogus");
-
-                    }
-                    return null;
-                }));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.equals("/oldlocation/")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "xxx://bogus");
+
+                            }
+                            return null;
+                        })));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> {
             final Future<SimpleHttpResponse> future = client.execute(
@@ -517,20 +511,20 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testRejectInvalidRedirectLocation() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.equals("/oldlocation/")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/newlocation/?p=I have spaces");
-
-                    }
-                    return null;
-                }));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.equals("/oldlocation/")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/newlocation/?p=I have spaces");
+                            }
+                            return null;
+                        })));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> {
             final Future<SimpleHttpResponse> future = client.execute(
@@ -545,16 +539,16 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
 
     @Test
     public void testRedirectWithCookie() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)));
-
-        final CookieStore cookieStore = new BasicCookieStore();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY))));
+        final HttpHost target = startServer();
 
-        final T client = startClient();
+        final TestAsyncClient client = startClient();
 
+        final CookieStore cookieStore = new BasicCookieStore();
         final HttpClientContext context = HttpClientContext.create();
         context.setCookieStore(cookieStore);
 
@@ -584,38 +578,34 @@ public abstract class AbstractHttpAsyncRedirectsTest <T extends CloseableHttpAsy
     @Test
     public void testCrossSiteRedirect() throws Exception {
         final URIScheme scheme = scheme();
-        final H2TestServer secondServer = new H2TestServer(IOReactorConfig.DEFAULT,
-                scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null, null, null);
+        final TestAsyncServer secondServer = new TestAsyncServerBootstrap(scheme(), getServerProtocolLevel())
+                .register("/random/*", AsyncRandomHandler::new)
+                .build();
         try {
-            secondServer.register("/random/*", AsyncRandomHandler::new);
-            final InetSocketAddress address2;
-            if (version.greaterEquals(HttpVersion.HTTP_2)) {
-                address2 = secondServer.start(H2Config.DEFAULT);
-            } else {
-                address2 = secondServer.start(Http1Config.DEFAULT);
-            }
-            final HttpHost redirectTarget = new HttpHost(scheme.name(), "localhost", address2.getPort());
+            final InetSocketAddress address2 = secondServer.start();
 
-            final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                    exchangeHandler,
-                    requestUri -> {
-                        final String path = requestUri.getPath();
-                        if (path.equals("/oldlocation")) {
-                            final URI location = new URIBuilder(requestUri)
-                                    .setHttpHost(redirectTarget)
-                                    .setPath("/random/100")
-                                    .build();
-                            return new Redirect(HttpStatus.SC_MOVED_PERMANENTLY, location.toString());
-                        }
-                        return null;
-                    }));
-
-            server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
-
-        final T client = startClient();
+            final HttpHost redirectTarget = new HttpHost(scheme.name(), "localhost", address2.getPort());
 
-        final HttpClientContext context = HttpClientContext.create();
+            configureServer(bootstrap -> bootstrap
+                    .register("/random/*", AsyncRandomHandler::new)
+                    .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                            exchangeHandler,
+                            requestUri -> {
+                                final String path = requestUri.getPath();
+                                if (path.equals("/oldlocation")) {
+                                    final URI location = new URIBuilder(requestUri)
+                                            .setHttpHost(redirectTarget)
+                                            .setPath("/random/100")
+                                            .build();
+                                    return new Redirect(HttpStatus.SC_MOVED_PERMANENTLY, location.toString());
+                                }
+                                return null;
+                            })));
+            final HttpHost target = startServer();
+
+            final TestAsyncClient client = startClient();
+
+            final HttpClientContext context = HttpClientContext.create();
             final Future<SimpleHttpResponse> future = client.execute(
                     SimpleRequestBuilder.get()
                             .setHttpHost(target)
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpReactiveFundamentalsTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpReactiveFundamentalsTest.java
index 04b8cafb9..1d2df6cd3 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpReactiveFundamentalsTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpReactiveFundamentalsTest.java
@@ -51,8 +51,10 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import io.reactivex.rxjava3.core.Flowable;
 import io.reactivex.rxjava3.schedulers.Schedulers;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.core5.concurrent.FutureCallback;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHost;
@@ -64,7 +66,6 @@ import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder;
 import org.apache.hc.core5.reactive.ReactiveEntityProducer;
 import org.apache.hc.core5.reactive.ReactiveResponseConsumer;
 import org.apache.hc.core5.reactive.ReactiveServerExchangeHandler;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.apache.hc.core5.testing.reactive.Reactive3TestUtils;
 import org.apache.hc.core5.testing.reactive.Reactive3TestUtils.StreamDescription;
 import org.apache.hc.core5.testing.reactive.ReactiveEchoProcessor;
@@ -76,24 +77,20 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 import org.reactivestreams.Publisher;
 
-public abstract class AbstractHttpReactiveFundamentalsTest<T extends CloseableHttpAsyncClient> extends AbstractIntegrationTestBase {
+public abstract class AbstractHttpReactiveFundamentalsTest extends AbstractIntegrationTestBase {
 
-    public AbstractHttpReactiveFundamentalsTest(final URIScheme scheme) {
-        super(scheme);
+    public AbstractHttpReactiveFundamentalsTest(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
+        super(scheme, clientProtocolLevel, serverProtocolLevel);
     }
 
-    abstract protected H2TestServer startServer() throws Exception;
-
-    abstract protected T startClient() throws Exception;
-
     @Test
     @Timeout(value = 60_000, unit = MILLISECONDS)
     public void testSequentialGetRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor()));
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", () -> new ReactiveServerExchangeHandler(new ReactiveRandomProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
         for (int i = 0; i < 3; i++) {
             final ReactiveResponseConsumer consumer = new ReactiveResponseConsumer();
 
@@ -112,11 +109,11 @@ public abstract class AbstractHttpReactiveFundamentalsTest<T extends CloseableHt
     @Test
     @Timeout(value = 2000, unit = MILLISECONDS)
     public void testSequentialHeadRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor()));
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
         for (int i = 0; i < 3; i++) {
             final ReactiveResponseConsumer consumer = new ReactiveResponseConsumer();
 
@@ -134,11 +131,11 @@ public abstract class AbstractHttpReactiveFundamentalsTest<T extends CloseableHt
     @Test
     @Timeout(value = 60_000, unit = MILLISECONDS)
     public void testSequentialPostRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/echo/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveEchoProcessor()));
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/echo/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveEchoProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
         for (int i = 0; i < 3; i++) {
             final byte[] b1 = new byte[1024];
             final Random rnd = new Random(System.currentTimeMillis());
@@ -164,11 +161,11 @@ public abstract class AbstractHttpReactiveFundamentalsTest<T extends CloseableHt
     @Test
     @Timeout(value = 60_000, unit = MILLISECONDS)
     public void testConcurrentPostRequests() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/echo/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveEchoProcessor()));
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/echo/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveEchoProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
         final int reqCount = 500;
         final int maxSize = 128 * 1024;
@@ -213,11 +210,11 @@ public abstract class AbstractHttpReactiveFundamentalsTest<T extends CloseableHt
     @Test
     @Timeout(value = 60_000, unit = MILLISECONDS)
     public void testRequestExecutionFromCallback() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor()));
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
         final int requestNum = 50;
         final AtomicInteger count = new AtomicInteger(requestNum);
         final Queue<Message<HttpResponse, Publisher<ByteBuffer>>> resultQueue = new ConcurrentLinkedQueue<>();
@@ -275,11 +272,11 @@ public abstract class AbstractHttpReactiveFundamentalsTest<T extends CloseableHt
 
     @Test
     public void testBadRequest() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor()));
-        final HttpHost target = targetHost();
-        final T client = startClient();
+        configureServer(bootstrap -> bootstrap.register("/random/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
         final AsyncRequestProducer request = AsyncRequestBuilder.get(target + "/random/boom").build();
         final ReactiveResponseConsumer consumer = new ReactiveResponseConsumer();
 
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java
index 3e80e3e86..09867e97a 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java
@@ -27,24 +27,18 @@
 
 package org.apache.hc.client5.testing.async;
 
+import java.net.InetSocketAddress;
 import java.util.function.Consumer;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
-import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
-import org.apache.hc.client5.http.impl.async.MinimalH2AsyncClient;
-import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient;
-import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
-import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClientBuilder;
 import org.apache.hc.client5.testing.async.extension.TestAsyncResources;
-import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.client5.testing.async.extension.TestAsyncServer;
+import org.apache.hc.client5.testing.async.extension.TestAsyncServerBootstrap;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.http.protocol.HttpProcessor;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
@@ -55,68 +49,40 @@ public abstract class AbstractIntegrationTestBase {
     @RegisterExtension
     private final TestAsyncResources testResources;
 
-    protected AbstractIntegrationTestBase(final URIScheme scheme) {
-        this.testResources = new TestAsyncResources(scheme, TIMEOUT);
+    protected AbstractIntegrationTestBase(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
+        this.testResources = new TestAsyncResources(scheme, clientProtocolLevel, serverProtocolLevel, TIMEOUT);
     }
 
     public URIScheme scheme() {
         return testResources.scheme();
     }
 
-    public H2TestServer startServer(
-            final H2Config h2Config,
-            final HttpProcessor httpProcessor,
-            final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        return testResources.startServer(h2Config, httpProcessor, exchangeHandlerDecorator);
+    public ServerProtocolLevel getServerProtocolLevel() {
+        return testResources.getServerProtocolLevel();
     }
 
-    public H2TestServer startServer(
-            final Http1Config http1Config,
-            final HttpProcessor httpProcessor,
-            final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        return testResources.startServer(http1Config, httpProcessor, exchangeHandlerDecorator);
+    public ClientProtocolLevel getClientProtocolLevel() {
+        return testResources.getClientProtocolLevel();
     }
 
-    public HttpHost targetHost() {
-        return testResources.targetHost();
+    public void configureServer(final Consumer<TestAsyncServerBootstrap> serverCustomizer) {
+        testResources.configureServer(serverCustomizer);
     }
 
-    public CloseableHttpAsyncClient startClient(
-            final Consumer<PoolingAsyncClientConnectionManagerBuilder> connManagerCustomizer,
-            final Consumer<HttpAsyncClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startClient(connManagerCustomizer, clientCustomizer);
+    public HttpHost startServer() throws Exception {
+        final TestAsyncServer server = testResources.server();
+        final InetSocketAddress inetSocketAddress = server.start();
+        return new HttpHost(testResources.scheme().id, "localhost", inetSocketAddress.getPort());
     }
 
-    public CloseableHttpAsyncClient startClient(
-            final Consumer<HttpAsyncClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startClient(clientCustomizer);
+    public void configureClient(final Consumer<TestAsyncClientBuilder> clientCustomizer) {
+        testResources.configureClient(clientCustomizer);
     }
 
-    public PoolingAsyncClientConnectionManager connManager() {
-        return testResources.connManager();
-    }
-
-    public CloseableHttpAsyncClient startH2Client(
-            final Consumer<H2AsyncClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startH2Client(clientCustomizer);
-    }
-
-    public MinimalHttpAsyncClient startMinimalClient(
-            final Http1Config http1Config,
-            final H2Config h2Config,
-            final Consumer<PoolingAsyncClientConnectionManagerBuilder> connManagerCustomizer) throws Exception {
-        return testResources.startMinimalClient(http1Config, h2Config, connManagerCustomizer);
-    }
-
-    public MinimalHttpAsyncClient startMinimalH2Client(
-            final Http1Config http1Config,
-            final H2Config h2Config,
-            final Consumer<PoolingAsyncClientConnectionManagerBuilder> connManagerCustomizer) throws Exception {
-        return testResources.startMinimalClient(http1Config, h2Config, connManagerCustomizer);
-    }
-
-    public MinimalH2AsyncClient startMinimalH2Client(final H2Config h2Config) throws Exception {
-        return testResources.startMinimalH2Client(h2Config);
+    public TestAsyncClient startClient() throws Exception {
+        final TestAsyncClient client = testResources.client();
+        client.start();
+        return client;
     }
 
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/HttpIntegrationTests.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/HttpIntegrationTests.java
index 452919c4d..df5da9203 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/HttpIntegrationTests.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/HttpIntegrationTests.java
@@ -173,44 +173,44 @@ public class HttpIntegrationTests {
 
     }
 
-    @Nested
-    @DisplayName("Client authentication (HTTP/1.1)")
-    public class AuthenticationHttp1 extends TestHttp1ClientAuthentication {
-
-        public AuthenticationHttp1() throws Exception {
-            super(URIScheme.HTTP);
-        }
-
-    }
-
-    @Nested
-    @DisplayName("Client authentication (HTTP/1.1, TLS)")
-    public class AuthenticationHttp1Tls extends TestHttp1ClientAuthentication {
-
-        public AuthenticationHttp1Tls() throws Exception {
-            super(URIScheme.HTTPS);
-        }
-
-    }
-
-    @Nested
-    @DisplayName("Client authentication (HTTP/2)")
-    public class AuthenticationH2 extends TestH2ClientAuthentication {
-
-        public AuthenticationH2() throws Exception {
-            super(URIScheme.HTTP);
-        }
-
-    }
-
-    @Nested
-    @DisplayName("Client authentication (HTTP/2, TLS)")
-    public class AuthenticationH2Tls extends TestH2ClientAuthentication {
-
-        public AuthenticationH2Tls() throws Exception {
-            super(URIScheme.HTTPS);
-        }
-
-    }
-
+//    @Nested
+//    @DisplayName("Client authentication (HTTP/1.1)")
+//    public class AuthenticationHttp1 extends TestHttp1ClientAuthentication {
+//
+//        public AuthenticationHttp1() throws Exception {
+//            super(URIScheme.HTTP);
+//        }
+//
+//    }
+//
+//    @Nested
+//    @DisplayName("Client authentication (HTTP/1.1, TLS)")
+//    public class AuthenticationHttp1Tls extends TestHttp1ClientAuthentication {
+//
+//        public AuthenticationHttp1Tls() throws Exception {
+//            super(URIScheme.HTTPS);
+//        }
+//
+//    }
+
+//    @Nested
+//    @DisplayName("Client authentication (HTTP/2)")
+//    public class AuthenticationH2 extends TestH2ClientAuthentication {
+//
+//        public AuthenticationH2() throws Exception {
+//            super(URIScheme.HTTP);
+//        }
+//
+//    }
+//
+//    @Nested
+//    @DisplayName("Client authentication (HTTP/2, TLS)")
+//    public class AuthenticationH2Tls extends TestH2ClientAuthentication {
+//
+//        public AuthenticationH2Tls() throws Exception {
+//            super(URIScheme.HTTPS);
+//        }
+//
+//    }
+//
 }
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
index e6485229c..148df77a7 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
@@ -26,25 +26,14 @@
  */
 package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 
-public abstract class TestH2Async extends AbstractHttpAsyncFundamentalsTest<CloseableHttpAsyncClient> {
+public abstract class TestH2Async extends AbstractHttpAsyncFundamentalsTest {
 
     public TestH2Async(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startH2Client(b -> {});
+        super(scheme, ClientProtocolLevel.H2_ONLY, ServerProtocolLevel.H2_ONLY);
     }
 
 }
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncMinimal.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncMinimal.java
index 81785aa34..0b1c1cea8 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncMinimal.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncMinimal.java
@@ -26,25 +26,14 @@
  */
 package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.MinimalH2AsyncClient;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 
-public abstract class TestH2AsyncMinimal extends AbstractHttpAsyncFundamentalsTest<MinimalH2AsyncClient> {
+public abstract class TestH2AsyncMinimal extends AbstractHttpAsyncFundamentalsTest {
 
     public TestH2AsyncMinimal(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected MinimalH2AsyncClient startClient() throws Exception {
-        return startMinimalH2Client(H2Config.DEFAULT);
+        super(scheme, ClientProtocolLevel.MINIMAL_H2_ONLY, ServerProtocolLevel.H2_ONLY);
     }
 
 }
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncRedirect.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncRedirect.java
index 78d7aebd5..b2a08ad7c 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncRedirect.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2AsyncRedirect.java
@@ -26,28 +26,14 @@
  */
 package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.core5.function.Decorator;
-import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 
-public abstract class TestH2AsyncRedirect extends AbstractHttpAsyncRedirectsTest<CloseableHttpAsyncClient> {
+public abstract class TestH2AsyncRedirect extends AbstractHttpAsyncRedirectsTest {
 
     public TestH2AsyncRedirect(final URIScheme scheme) {
-        super(scheme, HttpVersion.HTTP_2);
-    }
-
-    @Override
-    protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        return startServer(H2Config.DEFAULT, null, exchangeHandlerDecorator);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startH2Client(b -> {});
+        super(scheme, ClientProtocolLevel.H2_ONLY, ServerProtocolLevel.H2_ONLY);
     }
 
 }
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
index af0f21a40..f5ffb8742 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
@@ -26,71 +26,14 @@
  */
 package org.apache.hc.client5.testing.async;
 
-import java.util.function.Consumer;
-
-import org.apache.hc.client5.http.AuthenticationStrategy;
-import org.apache.hc.client5.http.auth.AuthSchemeFactory;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
-import org.apache.hc.core5.function.Decorator;
-import org.apache.hc.core5.http.HttpRequestInterceptor;
-import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Lookup;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 
-public abstract class TestH2ClientAuthentication extends AbstractHttpAsyncClientAuthenticationTest<CloseableHttpAsyncClient> {
+public abstract class TestH2ClientAuthentication extends AbstractHttpAsyncClientAuthenticationTest {
 
     public TestH2ClientAuthentication(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        return startServer(H2Config.DEFAULT, null, exchangeHandlerDecorator);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClientCustom(final Consumer<TestClientBuilder> clientCustomizer) throws Exception {
-
-        return startH2Client(new Consumer<H2AsyncClientBuilder>() {
-
-            @Override
-            public void accept(final H2AsyncClientBuilder builder) {
-
-                clientCustomizer.accept(new TestClientBuilder() {
-
-                    @Override
-                    public TestClientBuilder setDefaultAuthSchemeRegistry(final Lookup<AuthSchemeFactory> authSchemeRegistry) {
-                        builder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
-                        return this;
-                    }
-
-                    @Override
-                    public TestClientBuilder setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStrategy) {
-                        builder.setTargetAuthenticationStrategy(targetAuthStrategy);
-                        return this;
-                    }
-
-                    @Override
-                    public TestClientBuilder addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) {
-                        builder.addResponseInterceptorLast(responseInterceptor);
-                        return this;
-                    }
-
-                    @Override
-                    public TestClientBuilder addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) {
-                        builder.addRequestInterceptorFirst(requestInterceptor);
-                        return this;
-                    }
-
-                });
-
-            }
-
-        });
+        super(scheme, ClientProtocolLevel.H2_ONLY, ServerProtocolLevel.H2_ONLY);
     }
 
 }
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Reactive.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Reactive.java
index 50d940335..81b748e3c 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Reactive.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Reactive.java
@@ -26,25 +26,14 @@
  */
 package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 
-public abstract class TestH2Reactive extends AbstractHttpReactiveFundamentalsTest<CloseableHttpAsyncClient> {
+public abstract class TestH2Reactive extends AbstractHttpReactiveFundamentalsTest {
 
     public TestH2Reactive(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startH2Client(b -> {});
+        super(scheme, ClientProtocolLevel.H2_ONLY, ServerProtocolLevel.H2_ONLY);
     }
 
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ReactiveMinimal.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ReactiveMinimal.java
index ffaf9104f..8fe7ce12b 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ReactiveMinimal.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ReactiveMinimal.java
@@ -26,26 +26,14 @@
  */
 package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.MinimalH2AsyncClient;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 
-public abstract class TestH2ReactiveMinimal extends AbstractHttpReactiveFundamentalsTest<MinimalH2AsyncClient> {
+public abstract class TestH2ReactiveMinimal extends AbstractHttpReactiveFundamentalsTest {
 
     public TestH2ReactiveMinimal(final URIScheme scheme) {
-        super(scheme);
+        super(scheme, ClientProtocolLevel.MINIMAL_H2_ONLY, ServerProtocolLevel.H2_ONLY);
     }
 
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected MinimalH2AsyncClient startClient() throws Exception {
-        return startMinimalH2Client(H2Config.DEFAULT);
-    }
-
-
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Async.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Async.java
index 3e20f86ad..9c85e4421 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Async.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Async.java
@@ -40,42 +40,33 @@ import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
 import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 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.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
-public abstract class TestHttp1Async extends AbstractHttpAsyncFundamentalsTest<CloseableHttpAsyncClient> {
+public abstract class TestHttp1Async extends AbstractHttpAsyncFundamentalsTest {
 
     public TestHttp1Async(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(Http1Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startClient(b -> {});
+        super(scheme, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
     }
 
     @ParameterizedTest(name = "{displayName}; concurrent connections: {0}")
     @ValueSource(ints = {5, 1, 20})
     public void testSequentialGetRequestsCloseConnection(final int concurrentConns) throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
-        final CloseableHttpAsyncClient client = startClient();
-        final PoolingAsyncClientConnectionManager connManager = connManager();
+        final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
         connManager.setDefaultMaxPerRoute(concurrentConns);
         connManager.setMaxTotal(100);
         for (int i = 0; i < 3; i++) {
@@ -96,12 +87,12 @@ public abstract class TestHttp1Async extends AbstractHttpAsyncFundamentalsTest<C
 
     @Test
     public void testSharedPool() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
-        final CloseableHttpAsyncClient client = startClient();
-        final PoolingAsyncClientConnectionManager connManager = connManager();
+        final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
         final Future<SimpleHttpResponse> future1 = client.execute(
                 SimpleRequestBuilder.get()
                         .setHttpHost(target)
@@ -148,12 +139,11 @@ public abstract class TestHttp1Async extends AbstractHttpAsyncFundamentalsTest<C
 
     @Test
     public void testRequestCancellation() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
 
-        final CloseableHttpAsyncClient client = startClient();
-        final PoolingAsyncClientConnectionManager connManager = connManager();
+        final TestAsyncClient client = startClient();
+        final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
         connManager.setDefaultMaxPerRoute(1);
         connManager.setMaxTotal(1);
 
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncMinimal.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncMinimal.java
index e801b410a..7d6302a57 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncMinimal.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncMinimal.java
@@ -36,49 +36,34 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.Message;
 import org.apache.hc.core5.http.Method;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
 import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers;
 import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer;
 import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
 import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
 
-public abstract class TestHttp1AsyncMinimal extends AbstractHttpAsyncFundamentalsTest<MinimalHttpAsyncClient> {
+public abstract class TestHttp1AsyncMinimal extends AbstractHttpAsyncFundamentalsTest {
 
     public TestHttp1AsyncMinimal(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(Http1Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected MinimalHttpAsyncClient startClient() throws Exception {
-        return startMinimalClient(
-                Http1Config.DEFAULT,
-                H2Config.DEFAULT,
-                b -> {});
+        super(scheme, ClientProtocolLevel.MINIMAL, ServerProtocolLevel.STANDARD);
     }
 
     @Test
     public void testConcurrentPostRequestsSameEndpoint() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/echo/*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/echo/*", AsyncEchoHandler::new));
+        final HttpHost target = startServer();
 
-        final MinimalHttpAsyncClient client = startClient();
+        final MinimalHttpAsyncClient client = startClient().getImplementation();
 
         final byte[] b1 = new byte[1024];
         final Random rnd = new Random(System.currentTimeMillis());
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncRedirects.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncRedirects.java
index 77d47461e..4a143a75d 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncRedirects.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncRedirects.java
@@ -32,55 +32,43 @@ import java.util.concurrent.Future;
 
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.OldPathRedirectResolver;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.client5.testing.redirect.Redirect;
-import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.Header;
 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.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.message.BasicHeader;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 /**
  * Redirection test cases.
  */
-public abstract class TestHttp1AsyncRedirects extends AbstractHttpAsyncRedirectsTest<CloseableHttpAsyncClient> {
+public abstract class TestHttp1AsyncRedirects extends AbstractHttpAsyncRedirectsTest {
 
     public TestHttp1AsyncRedirects(final URIScheme scheme) {
-        super(scheme, HttpVersion.HTTP_1_1);
-    }
-
-    @Override
-    protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        return startServer(Http1Config.DEFAULT, null, exchangeHandlerDecorator);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startClient(b -> {});
+        super(scheme, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
     }
 
     @Test
     public void testBasicRedirect300NoKeepAlive() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES,
-                        Redirect.ConnControl.CLOSE)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES,
+                                Redirect.ConnControl.CLOSE))));
+        final HttpHost target = startServer();
 
-        final CloseableHttpAsyncClient client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
@@ -98,14 +86,15 @@ public abstract class TestHttp1AsyncRedirects extends AbstractHttpAsyncRedirects
 
     @Test
     public void testBasicRedirect301NoKeepAlive() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY,
-                        Redirect.ConnControl.CLOSE)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY,
+                                Redirect.ConnControl.CLOSE))));
+        final HttpHost target = startServer();
 
-        final CloseableHttpAsyncClient client = startClient();
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
@@ -124,18 +113,20 @@ public abstract class TestHttp1AsyncRedirects extends AbstractHttpAsyncRedirects
 
     @Test
     public void testDefaultHeadersRedirect() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler -> new RedirectingAsyncDecorator(
-                exchangeHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY,
-                        Redirect.ConnControl.CLOSE)));
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", AsyncRandomHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler -> new RedirectingAsyncDecorator(
+                        exchangeHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY,
+                                Redirect.ConnControl.CLOSE))));
+        final HttpHost target = startServer();
 
         final List<Header> defaultHeaders = new ArrayList<>(1);
         defaultHeaders.add(new BasicHeader(HttpHeaders.USER_AGENT, "my-test-client"));
-        final CloseableHttpAsyncClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setDefaultHeaders(defaultHeaders)
         );
+        final TestAsyncClient client = startClient();
 
         final HttpClientContext context = HttpClientContext.create();
         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncStatefulConnManagement.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncStatefulConnManagement.java
index 04880fe57..56ab02303 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncStatefulConnManagement.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1AsyncStatefulConnManagement.java
@@ -34,6 +34,9 @@ import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.EndpointDetails;
 import org.apache.hc.core5.http.HttpException;
@@ -41,28 +44,21 @@ import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
 import org.apache.hc.core5.net.URIAuthority;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 public class TestHttp1AsyncStatefulConnManagement extends AbstractIntegrationTestBase {
 
     public TestHttp1AsyncStatefulConnManagement() {
-        super(URIScheme.HTTP);
-    }
-
-    protected H2TestServer startServer() throws Exception {
-        return startServer(Http1Config.DEFAULT, null, null);
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
     }
 
     @Test
     public void testStatefulConnections() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", () -> new AbstractSimpleServerExchangeHandler() {
+        configureServer(bootstrap -> bootstrap.register("*", () -> new AbstractSimpleServerExchangeHandler() {
 
             @Override
             protected SimpleHttpResponse handle(
@@ -72,13 +68,14 @@ public class TestHttp1AsyncStatefulConnManagement extends AbstractIntegrationTes
                 response.setBody("Whatever", ContentType.TEXT_PLAIN);
                 return response;
             }
-        });
+        }));
+        final HttpHost target = startServer();
 
-        final HttpHost target = targetHost();
-
-        final CloseableHttpAsyncClient client = startClient(builer -> builer
+        configureClient(builder -> builder
                 .setUserTokenHandler((route, context) -> context.getAttribute("user")));
 
+        final TestAsyncClient client = startClient();
+
         final int workerCount = 2;
         final int requestCount = 5;
 
@@ -177,8 +174,7 @@ public class TestHttp1AsyncStatefulConnManagement extends AbstractIntegrationTes
 
     @Test
     public void testRouteSpecificPoolRecylcing() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("*", () -> new AbstractSimpleServerExchangeHandler() {
+        configureServer(bootstrap -> bootstrap.register("*", () -> new AbstractSimpleServerExchangeHandler() {
 
             @Override
             protected SimpleHttpResponse handle(
@@ -188,17 +184,19 @@ public class TestHttp1AsyncStatefulConnManagement extends AbstractIntegrationTes
                 response.setBody("Whatever", ContentType.TEXT_PLAIN);
                 return response;
             }
-        });
+        }));
+        final HttpHost target = startServer();
 
         // This tests what happens when a maxed connection pool needs
         // to kill the last idle connection to a route to build a new
         // one to the same route.
 
-        final HttpHost target = targetHost();
-
-        final CloseableHttpAsyncClient client = startClient(builer -> builer
+        configureClient(builder -> builder
                 .setUserTokenHandler((route, context) -> context.getAttribute("user")));
-        final PoolingAsyncClientConnectionManager connManager = connManager();
+
+        final TestAsyncClient client = startClient();
+
+        final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
 
         final int maxConn = 2;
         // We build a client with 2 max active // connections, and 2 max per route.
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
index 00702a64b..6489d11bc 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
@@ -27,103 +27,49 @@
 package org.apache.hc.client5.testing.async;
 
 import java.util.concurrent.Future;
-import java.util.function.Consumer;
 
-import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
-import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.BasicTestAuthenticator;
-import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 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.HttpRequestInterceptor;
 import org.apache.hc.core5.http.HttpResponse;
-import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.http.config.Lookup;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
-public abstract class TestHttp1ClientAuthentication extends AbstractHttpAsyncClientAuthenticationTest<CloseableHttpAsyncClient> {
+public abstract class TestHttp1ClientAuthentication extends AbstractHttpAsyncClientAuthenticationTest {
 
     public TestHttp1ClientAuthentication(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        return startServer(Http1Config.DEFAULT, null, exchangeHandlerDecorator);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClientCustom(final Consumer<TestClientBuilder> clientCustomizer) throws Exception {
-
-        return startClient(new Consumer<HttpAsyncClientBuilder>() {
-
-            @Override
-            public void accept(final HttpAsyncClientBuilder builder) {
-
-                clientCustomizer.accept(new TestClientBuilder() {
-
-                    @Override
-                    public TestClientBuilder setDefaultAuthSchemeRegistry(final Lookup<AuthSchemeFactory> authSchemeRegistry) {
-                        builder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
-                        return this;
-                    }
-
-                    @Override
-                    public TestClientBuilder setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStrategy) {
-                        builder.setTargetAuthenticationStrategy(targetAuthStrategy);
-                        return this;
-                    }
-
-                    @Override
-                    public TestClientBuilder addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) {
-                        builder.addResponseInterceptorLast(responseInterceptor);
-                        return this;
-                    }
-
-                    @Override
-                    public TestClientBuilder addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) {
-                        builder.addRequestInterceptorFirst(requestInterceptor);
-                        return this;
-                    }
-
-                });
-
-            }
-
-        });
+        super(scheme, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
     }
 
     @Test
     public void testBasicAuthenticationSuccessNonPersistentConnection() throws Exception {
-        final H2TestServer server = startServer(exchangeHandler ->
-                new AuthenticatingAsyncDecorator(exchangeHandler, new BasicTestAuthenticator("test:test", "test realm")) {
-
-                    @Override
-                    protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
-                        unauthorized.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE);
-                    }
-                });
-        server.register("*", AsyncEchoHandler::new);
-        final HttpHost target = targetHost();
-
-        final CloseableHttpAsyncClient client = startClient();
+        final HttpHost target = startServer();
+        configureServer(bootstrap -> bootstrap
+                .register("*", AsyncEchoHandler::new)
+                .setExchangeHandlerDecorator(exchangeHandler ->
+                        new AuthenticatingAsyncDecorator(exchangeHandler, new BasicTestAuthenticator("test:test", "test realm")) {
+
+                            @Override
+                            protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
+                                unauthorized.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE);
+                            }
+                        }));
+
+        final TestAsyncClient client = startClient();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Reactive.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Reactive.java
index e0d6fe336..7e9864374 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Reactive.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1Reactive.java
@@ -36,18 +36,19 @@ import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
 import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 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.HttpResponse;
 import org.apache.hc.core5.http.Message;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.nio.AsyncRequestProducer;
 import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder;
 import org.apache.hc.core5.reactive.ReactiveResponseConsumer;
 import org.apache.hc.core5.reactive.ReactiveServerExchangeHandler;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.apache.hc.core5.testing.reactive.ReactiveRandomProcessor;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
@@ -56,33 +57,23 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.reactivestreams.Publisher;
 
-public abstract class TestHttp1Reactive extends AbstractHttpReactiveFundamentalsTest<CloseableHttpAsyncClient> {
+public abstract class TestHttp1Reactive extends AbstractHttpReactiveFundamentalsTest {
 
     public TestHttp1Reactive(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(Http1Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startClient(b -> {});
+        super(scheme, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
     }
 
     @ParameterizedTest(name = "{displayName}; concurrent connections: {0}")
     @ValueSource(ints = {5, 1, 20})
     @Timeout(value = 60_000, unit = MILLISECONDS)
     public void testSequentialGetRequestsCloseConnection(final int concurrentConns) throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor()));
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
-        final CloseableHttpAsyncClient client = startClient();
-        final PoolingAsyncClientConnectionManager connManager = connManager();
+        final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
         connManager.setDefaultMaxPerRoute(concurrentConns);
         connManager.setMaxTotal(100);
 
@@ -109,13 +100,13 @@ public abstract class TestHttp1Reactive extends AbstractHttpReactiveFundamentals
     @Test
     @Timeout(value = 60_000, unit = MILLISECONDS)
     public void testSharedPool() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor()));
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveRandomProcessor())));
+        final HttpHost target = startServer();
+
+        final TestAsyncClient client = startClient();
 
-        final CloseableHttpAsyncClient client = startClient();
-        final PoolingAsyncClientConnectionManager connManager = connManager();
+        final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
 
         final AsyncRequestProducer request1 = AsyncRequestBuilder.get(target + "/random/2048").build();
         final ReactiveResponseConsumer consumer1 = new ReactiveResponseConsumer();
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ReactiveMinimal.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ReactiveMinimal.java
index 1992fa519..18caa1cd2 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ReactiveMinimal.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ReactiveMinimal.java
@@ -36,52 +36,37 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.Message;
 import org.apache.hc.core5.http.Method;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
 import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers;
 import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer;
 import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
 import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
-import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.reactive.ReactiveServerExchangeHandler;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.apache.hc.core5.testing.reactive.ReactiveEchoProcessor;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
 
-public abstract class TestHttp1ReactiveMinimal extends AbstractHttpReactiveFundamentalsTest<MinimalHttpAsyncClient> {
+public abstract class TestHttp1ReactiveMinimal extends AbstractHttpReactiveFundamentalsTest {
 
     public TestHttp1ReactiveMinimal(final URIScheme scheme) {
-        super(scheme);
-    }
-
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(Http1Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected MinimalHttpAsyncClient startClient() throws Exception {
-        return startMinimalClient(
-                Http1Config.DEFAULT,
-                H2Config.DEFAULT,
-                b -> {});
+        super(scheme, ClientProtocolLevel.MINIMAL, ServerProtocolLevel.STANDARD);
     }
 
     @Test
     public void testConcurrentPostRequestsSameEndpoint() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/echo/*", () ->
-                new ReactiveServerExchangeHandler(new ReactiveEchoProcessor()));
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/echo/*", () ->
+                new ReactiveServerExchangeHandler(new ReactiveEchoProcessor())));
+        final HttpHost target = startServer();
 
-        final MinimalHttpAsyncClient client = startClient();
+        final MinimalHttpAsyncClient client = startClient().getImplementation();
 
         final byte[] b1 = new byte[1024];
         final Random rnd = new Random(System.currentTimeMillis());
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1RequestReExecution.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1RequestReExecution.java
index 50294e9bb..c877ed8c4 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1RequestReExecution.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1RequestReExecution.java
@@ -34,25 +34,27 @@ import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.core5.function.Resolver;
 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.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.apache.hc.core5.util.TimeValue;
 import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 public abstract class TestHttp1RequestReExecution extends AbstractIntegrationTestBase {
 
     public TestHttp1RequestReExecution(final URIScheme scheme) {
-        super(scheme);
+        super(scheme, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
     }
 
-    protected H2TestServer startServer() throws Exception {
+    @BeforeEach
+    public void setup() {
         final Resolver<HttpRequest, TimeValue> serviceAvailabilityResolver = new Resolver<HttpRequest, TimeValue>() {
 
             private final AtomicInteger count = new AtomicInteger(0);
@@ -65,18 +67,18 @@ public abstract class TestHttp1RequestReExecution extends AbstractIntegrationTes
 
         };
 
-        return startServer(Http1Config.DEFAULT, null, handler ->
-                new ServiceUnavailableAsyncDecorator(handler, serviceAvailabilityResolver));
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(handler -> new ServiceUnavailableAsyncDecorator(handler, serviceAvailabilityResolver)));
     }
 
     @Test
     public void testGiveUpAfterOneRetry() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
 
-        final CloseableHttpAsyncClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setRetryStrategy(new DefaultHttpRequestRetryStrategy(1, TimeValue.ofSeconds(1))));
+        final TestAsyncClient client = startClient();
 
         final Future<SimpleHttpResponse> future = client.execute(
                 SimpleRequestBuilder.get()
@@ -90,12 +92,12 @@ public abstract class TestHttp1RequestReExecution extends AbstractIntegrationTes
 
     @Test
     public void testDoNotGiveUpEasily() throws Exception {
-        final H2TestServer server = startServer();
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
 
-        final CloseableHttpAsyncClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setRetryStrategy(new DefaultHttpRequestRetryStrategy(5, TimeValue.ofSeconds(1))));
+        final TestAsyncClient client = startClient();
 
         final Future<SimpleHttpResponse> future = client.execute(
                 SimpleRequestBuilder.get()
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncMinimalTlsHandshake.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncMinimalTlsHandshake.java
index 86545fce9..ae3133ccf 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncMinimalTlsHandshake.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncMinimalTlsHandshake.java
@@ -29,18 +29,17 @@ package org.apache.hc.client5.testing.async;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
 
 import javax.net.ssl.SSLException;
 
 import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient;
-import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
 import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
-import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.ssl.SSLContexts;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -48,26 +47,19 @@ import org.junit.jupiter.api.Test;
 public class TestHttpAsyncMinimalTlsHandshake extends AbstractIntegrationTestBase {
 
     public TestHttpAsyncMinimalTlsHandshake() {
-        super(URIScheme.HTTPS);
-    }
-
-    protected MinimalHttpAsyncClient startMinimalClient(
-            final Consumer<PoolingAsyncClientConnectionManagerBuilder> connManagerCustomizer) throws Exception {
-        return startMinimalClient(
-                Http1Config.DEFAULT,
-                H2Config.DEFAULT,
-                connManagerCustomizer);
+        super(URIScheme.HTTPS, ClientProtocolLevel.MINIMAL, ServerProtocolLevel.STANDARD);
     }
 
     @Test
     public void testSuccessfulTlsHandshake() throws Exception {
-        startServer(Http1Config.DEFAULT, null, null);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
         final int maxConnNo = 2;
-        final MinimalHttpAsyncClient client = startMinimalClient(builder -> builder
-                        .setMaxConnPerRoute(maxConnNo)
-                        .setMaxConnTotal(maxConnNo));
+        final PoolingAsyncClientConnectionManager connectionManager = startClient().getConnectionManager();
+        connectionManager.setDefaultMaxPerRoute(maxConnNo);
+        connectionManager.setMaxTotal(maxConnNo);
+
+        final MinimalHttpAsyncClient client = startClient().getImplementation();
 
         for (int i = 0; i < maxConnNo + 1; i++) {
             final Future<AsyncClientEndpoint> endpointLease = client.lease(target, null);
@@ -78,14 +70,17 @@ public class TestHttpAsyncMinimalTlsHandshake extends AbstractIntegrationTestBas
 
     @Test
     public void testTlsHandshakeFailure() throws Exception {
-        startServer(Http1Config.DEFAULT, null, null);
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
+
+        configureClient(builder ->
+                builder.setTlsStrategy(new DefaultClientTlsStrategy(SSLContexts.createDefault())));
 
         final int maxConnNo = 2;
-        final MinimalHttpAsyncClient client = startMinimalClient(builder -> builder
-                .setTlsStrategy(new DefaultClientTlsStrategy(SSLContexts.createDefault()))
-                .setMaxConnPerRoute(maxConnNo)
-                .setMaxConnTotal(maxConnNo));
+        final PoolingAsyncClientConnectionManager connectionManager = startClient().getConnectionManager();
+        connectionManager.setDefaultMaxPerRoute(maxConnNo);
+        connectionManager.setMaxTotal(maxConnNo);
+
+        final MinimalHttpAsyncClient client = startClient().getImplementation();
 
         for (int i = 0; i < maxConnNo + 1; i++) {
             final Future<AsyncClientEndpoint> endpointLease = client.lease(target, null);
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncProtocolPolicy.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncProtocolPolicy.java
index aceeca195..743d21f0a 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncProtocolPolicy.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncProtocolPolicy.java
@@ -34,15 +34,14 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.config.TlsConfig;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.testing.async.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.ServerProtocolLevel;
+import org.apache.hc.client5.testing.async.extension.TestAsyncClient;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.ProtocolVersion;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http2.HttpVersionPolicy;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
 
@@ -51,31 +50,23 @@ public abstract class TestHttpAsyncProtocolPolicy extends AbstractIntegrationTes
     private final HttpVersion version;
 
     public TestHttpAsyncProtocolPolicy(final URIScheme scheme, final HttpVersion version) {
-        super(scheme);
+        super(scheme, ClientProtocolLevel.STANDARD, version == HttpVersion.HTTP_2 ? ServerProtocolLevel.H2_ONLY : ServerProtocolLevel.STANDARD);
         this.version = version;
     }
 
     @Test
     public void testRequestContext() throws Exception {
-        final H2TestServer server;
-        if (version.greaterEquals(HttpVersion.HTTP_2)) {
-            server = startServer(H2Config.DEFAULT, null, null);
-        } else {
-            server = startServer(Http1Config.DEFAULT, null, null);
-        }
-        server.register("/random/*", AsyncRandomHandler::new);
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
+        final HttpHost target = startServer();
 
         final AtomicReference<ProtocolVersion> versionRef = new AtomicReference<>();
-        final CloseableHttpAsyncClient client = startClient(
-                builder -> builder
-                        .setDefaultTlsConfig(TlsConfig.custom()
-                                .setVersionPolicy(version.greaterEquals(HttpVersion.HTTP_2) ? HttpVersionPolicy.FORCE_HTTP_2 : HttpVersionPolicy.FORCE_HTTP_1)
-                                .build()),
-                builder -> builder
-                        .addRequestInterceptorFirst((request, entity, context) ->
-                                versionRef.set(context.getProtocolVersion())
-                        ));
+        configureClient(builder -> builder
+                .setDefaultTlsConfig(TlsConfig.custom()
+                        .setVersionPolicy(version.greaterEquals(HttpVersion.HTTP_2) ? HttpVersionPolicy.FORCE_HTTP_2 : HttpVersionPolicy.FORCE_HTTP_1)
+                        .build())
+                .addRequestInterceptorFirst((request, entity, context) ->
+                        versionRef.set(context.getProtocolVersion())));
+        final TestAsyncClient client = startClient();
 
         final Future<SimpleHttpResponse> future = client.execute(
                 SimpleRequestBuilder.get()
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/ClientProtocolLevel.java
similarity index 62%
copy from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
copy to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/ClientProtocolLevel.java
index e6485229c..8e439efec 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/ClientProtocolLevel.java
@@ -24,27 +24,11 @@
  * <http://www.apache.org/>.
  *
  */
-package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
+package org.apache.hc.client5.testing.async.extension;
 
-public abstract class TestH2Async extends AbstractHttpAsyncFundamentalsTest<CloseableHttpAsyncClient> {
+public enum ClientProtocolLevel {
 
-    public TestH2Async(final URIScheme scheme) {
-        super(scheme);
-    }
+    STANDARD, H2_ONLY, MINIMAL, MINIMAL_H2_ONLY
 
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startH2Client(b -> {});
-    }
-
-}
\ No newline at end of file
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/H2OnlyMinimalTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/H2OnlyMinimalTestClientBuilder.java
new file mode 100644
index 000000000..e90f5e87c
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/H2OnlyMinimalTestClientBuilder.java
@@ -0,0 +1,82 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.Timeout;
+
+final class H2OnlyMinimalTestClientBuilder implements TestAsyncClientBuilder {
+
+    private Timeout timeout;
+    private TlsStrategy tlsStrategy;
+    private H2Config h2Config;
+
+    public H2OnlyMinimalTestClientBuilder() {
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTlsStrategy(final TlsStrategy tlsStrategy) {
+        this.tlsStrategy = tlsStrategy;
+        return this;
+    }
+
+    @Override
+    public ClientProtocolLevel getProtocolLevel() {
+        return ClientProtocolLevel.MINIMAL_H2_ONLY;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setH2Config(final H2Config h2Config) {
+        this.h2Config = h2Config;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClient build() throws Exception {
+        final CloseableHttpAsyncClient client = HttpAsyncClients.createHttp2Minimal(
+                h2Config,
+                IOReactorConfig.custom()
+                        .setSoTimeout(timeout)
+                        .build(),
+                tlsStrategy != null ? tlsStrategy : new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
+        return new TestAsyncClient(client, null);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/H2OnlyTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/H2OnlyTestClientBuilder.java
new file mode 100644
index 000000000..e70452d63
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/H2OnlyTestClientBuilder.java
@@ -0,0 +1,136 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import java.util.Collection;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.Timeout;
+
+final class H2OnlyTestClientBuilder implements TestAsyncClientBuilder {
+
+    private final H2AsyncClientBuilder clientBuilder;
+
+    private Timeout timeout;
+    private TlsStrategy tlsStrategy;
+    private H2Config h2Config;
+
+    public H2OnlyTestClientBuilder() {
+        this.clientBuilder = H2AsyncClientBuilder.create();
+    }
+
+    @Override
+    public ClientProtocolLevel getProtocolLevel() {
+        return ClientProtocolLevel.H2_ONLY;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
+        this.clientBuilder.addResponseInterceptorFirst(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addResponseInterceptorLast(final HttpResponseInterceptor interceptor) {
+        this.clientBuilder.addResponseInterceptorLast(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addRequestInterceptorFirst(final HttpRequestInterceptor interceptor) {
+        this.clientBuilder.addRequestInterceptorFirst(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addRequestInterceptorLast(final HttpRequestInterceptor interceptor) {
+        this.clientBuilder.addRequestInterceptorLast(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTlsStrategy(final TlsStrategy tlsStrategy) {
+        this.tlsStrategy = tlsStrategy;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setH2Config(final H2Config h2Config) {
+        this.h2Config = h2Config;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
+        this.clientBuilder.setDefaultHeaders(defaultHeaders);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStrategy) {
+        this.clientBuilder.setTargetAuthenticationStrategy(targetAuthStrategy);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setDefaultAuthSchemeRegistry(final Lookup<AuthSchemeFactory> authSchemeRegistry) {
+        this.clientBuilder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClient build() throws Exception {
+        final CloseableHttpAsyncClient client = clientBuilder
+                .setTlsStrategy(tlsStrategy != null ? tlsStrategy : new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                .setIOReactorConfig(IOReactorConfig.custom()
+                        .setSoTimeout(timeout)
+                        .build())
+                .setH2Config(h2Config)
+                .build();
+        return new TestAsyncClient(client, null);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/MinimalTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/MinimalTestClientBuilder.java
new file mode 100644
index 000000000..2c939ee1c
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/MinimalTestClientBuilder.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.Timeout;
+
+final class MinimalTestClientBuilder implements TestAsyncClientBuilder {
+
+    private final PoolingAsyncClientConnectionManagerBuilder connectionManagerBuilder;
+
+    private Timeout timeout;
+    private TlsStrategy tlsStrategy;
+    private Http1Config http1Config;
+    private H2Config h2Config;
+
+    public MinimalTestClientBuilder() {
+        this.connectionManagerBuilder = PoolingAsyncClientConnectionManagerBuilder.create();
+    }
+
+    @Override
+    public ClientProtocolLevel getProtocolLevel() {
+        return ClientProtocolLevel.MINIMAL;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTlsStrategy(final TlsStrategy tlsStrategy) {
+        this.tlsStrategy = tlsStrategy;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setDefaultTlsConfig(final TlsConfig tlsConfig) {
+        this.connectionManagerBuilder.setDefaultTlsConfig(tlsConfig);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setHttp1Config(final Http1Config http1Config) {
+        this.http1Config = http1Config;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setH2Config(final H2Config h2Config) {
+        this.h2Config = h2Config;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClient build() throws Exception {
+        final PoolingAsyncClientConnectionManager connectionManager = connectionManagerBuilder
+                .setTlsStrategy(tlsStrategy != null ? tlsStrategy : new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                .setDefaultConnectionConfig(ConnectionConfig.custom()
+                        .setSocketTimeout(timeout)
+                        .setConnectTimeout(timeout)
+                        .build())
+                .build();
+        final CloseableHttpAsyncClient client = HttpAsyncClients.createMinimal(
+                        h2Config,
+                        http1Config,
+                        IOReactorConfig.custom()
+                                .setSoTimeout(timeout)
+                                .build(),
+                connectionManager);
+        return new TestAsyncClient(client, connectionManager);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/ServerProtocolLevel.java
similarity index 62%
copy from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
copy to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/ServerProtocolLevel.java
index e6485229c..3a5690727 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/ServerProtocolLevel.java
@@ -24,27 +24,11 @@
  * <http://www.apache.org/>.
  *
  */
-package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
+package org.apache.hc.client5.testing.async.extension;
 
-public abstract class TestH2Async extends AbstractHttpAsyncFundamentalsTest<CloseableHttpAsyncClient> {
+public enum ServerProtocolLevel {
 
-    public TestH2Async(final URIScheme scheme) {
-        super(scheme);
-    }
+    STANDARD, H2_ONLY
 
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startH2Client(b -> {});
-    }
-
-}
\ No newline at end of file
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/StandardTestClientBuilder.java
new file mode 100644
index 000000000..88807d1cb
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/StandardTestClientBuilder.java
@@ -0,0 +1,174 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import java.util.Collection;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.UserTokenHandler;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.Timeout;
+
+final class StandardTestClientBuilder implements TestAsyncClientBuilder {
+
+    private final PoolingAsyncClientConnectionManagerBuilder connectionManagerBuilder;
+    private final HttpAsyncClientBuilder clientBuilder;
+
+    private Timeout timeout;
+    private TlsStrategy tlsStrategy;
+
+    public StandardTestClientBuilder() {
+        this.connectionManagerBuilder = PoolingAsyncClientConnectionManagerBuilder.create();
+        this.clientBuilder = HttpAsyncClientBuilder.create();
+    }
+
+    @Override
+    public ClientProtocolLevel getProtocolLevel() {
+        return ClientProtocolLevel.STANDARD;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
+        this.clientBuilder.addResponseInterceptorFirst(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addResponseInterceptorLast(final HttpResponseInterceptor interceptor) {
+        this.clientBuilder.addResponseInterceptorLast(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addRequestInterceptorFirst(final HttpRequestInterceptor interceptor) {
+        this.clientBuilder.addRequestInterceptorFirst(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder addRequestInterceptorLast(final HttpRequestInterceptor interceptor) {
+        this.clientBuilder.addRequestInterceptorLast(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTlsStrategy(final TlsStrategy tlsStrategy) {
+        this.tlsStrategy = tlsStrategy;
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setDefaultTlsConfig(final TlsConfig tlsConfig) {
+        this.connectionManagerBuilder.setDefaultTlsConfig(tlsConfig);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setHttp1Config(final Http1Config http1Config) {
+        this.clientBuilder.setHttp1Config(http1Config);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setH2Config(final H2Config h2Config) {
+        this.clientBuilder.setH2Config(h2Config);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setUserTokenHandler(final UserTokenHandler userTokenHandler) {
+        this.clientBuilder.setUserTokenHandler(userTokenHandler);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
+        this.clientBuilder.setDefaultHeaders(defaultHeaders);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
+        this.clientBuilder.setRetryStrategy(retryStrategy);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStrategy) {
+        this.clientBuilder.setTargetAuthenticationStrategy(targetAuthStrategy);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClientBuilder setDefaultAuthSchemeRegistry(final Lookup<AuthSchemeFactory> authSchemeRegistry) {
+        this.clientBuilder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+        return this;
+    }
+
+    @Override
+    public TestAsyncClient build() throws Exception {
+        final PoolingAsyncClientConnectionManager connectionManager = connectionManagerBuilder
+                .setTlsStrategy(tlsStrategy != null ? tlsStrategy : new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                .setDefaultConnectionConfig(ConnectionConfig.custom()
+                        .setSocketTimeout(timeout)
+                        .setConnectTimeout(timeout)
+                        .build())
+                .build();
+        final CloseableHttpAsyncClient client = clientBuilder
+                .setIOReactorConfig(IOReactorConfig.custom()
+                        .setSoTimeout(timeout)
+                        .build())
+                .setConnectionManager(connectionManager)
+                .build();
+        return new TestAsyncClient(client, connectionManager);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncClient.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncClient.java
new file mode 100644
index 000000000..41543fd6a
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncClient.java
@@ -0,0 +1,117 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import java.io.IOException;
+import java.util.concurrent.Future;
+
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.AsyncRequestProducer;
+import org.apache.hc.core5.http.nio.AsyncResponseConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.IOReactorStatus;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Asserts;
+import org.apache.hc.core5.util.TimeValue;
+
+public class TestAsyncClient extends CloseableHttpAsyncClient {
+
+    private final CloseableHttpAsyncClient client;
+    private final PoolingAsyncClientConnectionManager connectionManager;
+
+    public TestAsyncClient(final CloseableHttpAsyncClient client,
+                           final PoolingAsyncClientConnectionManager connectionManager) {
+        this.client = Args.notNull(client, "Client");
+        this.connectionManager = connectionManager;
+    }
+
+    @Override
+    public void close(final CloseMode closeMode) {
+        client.close(closeMode);
+    }
+
+    @Override
+    public void close() throws IOException {
+        client.close();
+    }
+
+    @Override
+    public void start() {
+        client.start();
+    }
+
+    @Override
+    public IOReactorStatus getStatus() {
+        return client.getStatus();
+    }
+
+    @Override
+    public void awaitShutdown(final TimeValue waitTime) throws InterruptedException {
+        client.awaitShutdown(waitTime);
+    }
+
+    @Override
+    public void initiateShutdown() {
+        client.initiateShutdown();
+    }
+
+    @Override
+    protected <T> Future<T> doExecute(final HttpHost target,
+                                      final AsyncRequestProducer requestProducer,
+                                      final AsyncResponseConsumer<T> responseConsumer,
+                                      final HandlerFactory<AsyncPushConsumer> pushHandlerFactory,
+                                      final HttpContext context,
+                                      final FutureCallback<T> callback) {
+        return client.execute(target, requestProducer, responseConsumer, pushHandlerFactory, context, callback);
+    }
+
+    @Override
+    public void register(final String hostname,
+                         final String uriPattern,
+                         final Supplier<AsyncPushConsumer> supplier) {
+        client.register(hostname, uriPattern, supplier);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends CloseableHttpAsyncClient> T getImplementation() {
+        return (T) client;
+    }
+
+    public PoolingAsyncClientConnectionManager getConnectionManager() {
+        Asserts.check(connectionManager != null, "Connection manager is not available");
+        return connectionManager;
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncClientBuilder.java
new file mode 100644
index 000000000..43276ec2f
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncClientBuilder.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import java.util.Collection;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.UserTokenHandler;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.util.Timeout;
+
+public interface TestAsyncClientBuilder {
+
+    ClientProtocolLevel getProtocolLevel();
+
+    TestAsyncClientBuilder setTimeout(Timeout soTimeout);
+
+    default TestAsyncClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
+        return this;
+    }
+
+    default TestAsyncClientBuilder addResponseInterceptorLast(HttpResponseInterceptor interceptor) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder addRequestInterceptorFirst(HttpRequestInterceptor interceptor) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder addRequestInterceptorLast(HttpRequestInterceptor interceptor) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setTlsStrategy(TlsStrategy tlsStrategy) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setDefaultTlsConfig(TlsConfig tlsConfig) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setHttp1Config(Http1Config http1Config) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setH2Config(H2Config http1Config) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setUserTokenHandler(UserTokenHandler userTokenHandler) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setDefaultHeaders(Collection<? extends Header> defaultHeaders) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setRetryStrategy(HttpRequestRetryStrategy retryStrategy) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestAsyncClientBuilder setDefaultAuthSchemeRegistry(Lookup<AuthSchemeFactory> authSchemeRegistry) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    TestAsyncClient build() throws Exception;
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncResources.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncResources.java
index 49a411e74..115d353a5 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncResources.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncResources.java
@@ -27,81 +27,63 @@
 
 package org.apache.hc.client5.testing.async.extension;
 
-import java.net.InetSocketAddress;
 import java.util.function.Consumer;
 
-import org.apache.hc.client5.http.config.ConnectionConfig;
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
-import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
-import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
-import org.apache.hc.client5.http.impl.async.MinimalH2AsyncClient;
-import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient;
-import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
-import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
-import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
-import org.apache.hc.client5.testing.SSLTestContexts;
 import org.apache.hc.client5.testing.sync.extension.TestClientResources;
-import org.apache.hc.core5.function.Decorator;
-import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
-import org.apache.hc.core5.http.protocol.HttpProcessor;
-import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.io.CloseMode;
-import org.apache.hc.core5.reactor.IOReactorConfig;
-import org.apache.hc.core5.testing.nio.H2TestServer;
+import org.apache.hc.core5.util.Asserts;
 import org.apache.hc.core5.util.TimeValue;
 import org.apache.hc.core5.util.Timeout;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.extension.AfterEachCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class TestAsyncResources implements BeforeEachCallback, AfterEachCallback {
+public class TestAsyncResources implements AfterEachCallback {
 
     private static final Logger LOG = LoggerFactory.getLogger(TestClientResources.class);
 
     private final URIScheme scheme;
-    private final Timeout timeout;
-
-    private H2TestServer server;
-    private InetSocketAddress socketAddress;
-    private PoolingAsyncClientConnectionManager connManager;
-    private CloseableHttpAsyncClient client;
-
-    public TestAsyncResources(final URIScheme scheme, final Timeout timeout) {
-        this.scheme = scheme;
-        this.timeout = timeout;
-    }
-
-    @Override
-    public void beforeEach(final ExtensionContext extensionContext) throws Exception {
-        LOG.debug("Starting up test server");
-        server = new H2TestServer(
-                IOReactorConfig.custom()
-                        .setSoTimeout(timeout)
-                        .build(),
-                scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null,
-                null,
-                null);
+    private final ClientProtocolLevel clientProtocolLevel;
+    private final ServerProtocolLevel serverProtocolLevel;
+    private final TestAsyncServerBootstrap serverBootstrap;
+    private final TestAsyncClientBuilder clientBuilder;
+
+    private TestAsyncServer server;
+    private TestAsyncClient client;
+
+    public TestAsyncResources(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel, final Timeout timeout) {
+        this.scheme = scheme != null ? scheme : URIScheme.HTTP;
+        this.clientProtocolLevel = clientProtocolLevel != null ? clientProtocolLevel : ClientProtocolLevel.STANDARD;
+        this.serverProtocolLevel = serverProtocolLevel != null ? serverProtocolLevel : ServerProtocolLevel.STANDARD;
+        this.serverBootstrap = new TestAsyncServerBootstrap(this.scheme, this.serverProtocolLevel)
+                .setTimeout(timeout);
+        switch (this.clientProtocolLevel) {
+            case STANDARD:
+                this.clientBuilder = new StandardTestClientBuilder();
+                break;
+            case H2_ONLY:
+                this.clientBuilder = new H2OnlyTestClientBuilder();
+                break;
+            case MINIMAL_H2_ONLY:
+                this.clientBuilder = new H2OnlyMinimalTestClientBuilder();
+                break;
+            case MINIMAL:
+                this.clientBuilder = new MinimalTestClientBuilder();
+                break;
+            default:
+                this.clientBuilder = new StandardTestClientBuilder();
+        }
+        this.clientBuilder.setTimeout(timeout);
     }
 
     @Override
     public void afterEach(final ExtensionContext extensionContext) throws Exception {
         LOG.debug("Shutting down test server");
-
         if (client != null) {
             client.close(CloseMode.GRACEFUL);
         }
-
-        if (connManager != null) {
-            connManager.close(CloseMode.IMMEDIATE);
-        }
-
         if (server != null) {
             server.shutdown(TimeValue.ofSeconds(5));
         }
@@ -111,125 +93,36 @@ public class TestAsyncResources implements BeforeEachCallback, AfterEachCallback
         return this.scheme;
     }
 
-    public H2TestServer startServer(
-            final H2Config h2Config,
-            final HttpProcessor httpProcessor,
-            final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        Assertions.assertNotNull(server);
-        socketAddress = server.start(httpProcessor, exchangeHandlerDecorator, h2Config);
-        return server;
-    }
-
-    public H2TestServer startServer(
-            final Http1Config http1Config,
-            final HttpProcessor httpProcessor,
-            final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception {
-        Assertions.assertNotNull(server);
-        socketAddress = server.start(httpProcessor, exchangeHandlerDecorator, http1Config);
-        return server;
-    }
-
-    public HttpHost targetHost() {
-        Assertions.assertNotNull(socketAddress);
-        return new HttpHost(scheme.id, "localhost", socketAddress.getPort());
+    public ServerProtocolLevel getServerProtocolLevel() {
+        return serverProtocolLevel;
     }
 
-    public CloseableHttpAsyncClient startClient(
-            final Consumer<PoolingAsyncClientConnectionManagerBuilder> connManagerCustomizer,
-            final Consumer<HttpAsyncClientBuilder> clientCustomizer) throws Exception {
-        Assertions.assertNull(connManager);
-        Assertions.assertNull(client);
-
-        final PoolingAsyncClientConnectionManagerBuilder connManagerBuilder = PoolingAsyncClientConnectionManagerBuilder.create();
-        connManagerBuilder.setTlsStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
-        connManagerBuilder.setDefaultConnectionConfig(ConnectionConfig.custom()
-                .setSocketTimeout(timeout)
-                .setConnectTimeout(timeout)
-                .build());
-        connManagerCustomizer.accept(connManagerBuilder);
-
-        connManager = connManagerBuilder.build();
-
-        final HttpAsyncClientBuilder clientBuilder = HttpAsyncClientBuilder.create()
-                .setConnectionManager(connManager)
-                .setIOReactorConfig(IOReactorConfig.custom()
-                        .setSoTimeout(timeout)
-                        .build());
-
-        clientCustomizer.accept(clientBuilder);
-        client = clientBuilder.build();
-        client.start();
-        return client;
+    public ClientProtocolLevel getClientProtocolLevel() {
+        return clientProtocolLevel;
     }
 
-    public CloseableHttpAsyncClient startClient(
-            final Consumer<HttpAsyncClientBuilder> clientCustomizer) throws Exception {
-        return startClient(b -> {
-        }, clientCustomizer);
+    public void configureServer(final Consumer<TestAsyncServerBootstrap> serverCustomizer) {
+        Asserts.check(server == null, "Server is already running and cannot be changed");
+        serverCustomizer.accept(serverBootstrap);
     }
 
-    public PoolingAsyncClientConnectionManager connManager() {
-        Assertions.assertNotNull(connManager);
-        return connManager;
+    public TestAsyncServer server() throws Exception {
+        if (server == null) {
+            server = serverBootstrap.build();
+        }
+        return server;
     }
 
-    public CloseableHttpAsyncClient startH2Client(
-            final Consumer<H2AsyncClientBuilder> clientCustomizer) throws Exception {
-        Assertions.assertNull(connManager);
-        Assertions.assertNull(client);
-
-        final H2AsyncClientBuilder clientBuilder = H2AsyncClientBuilder.create();
-        clientBuilder.setIOReactorConfig(IOReactorConfig.custom()
-                .setSoTimeout(timeout)
-                .build());
-        clientBuilder.setTlsStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
+    public void configureClient(final Consumer<TestAsyncClientBuilder> clientCustomizer) {
+        Asserts.check(client == null, "Client is already running and cannot be changed");
         clientCustomizer.accept(clientBuilder);
-        client = clientBuilder.build();
-        client.start();
-        return client;
     }
 
-    public MinimalHttpAsyncClient startMinimalClient(
-            final Http1Config http1Config,
-            final H2Config h2Config,
-            final Consumer<PoolingAsyncClientConnectionManagerBuilder> connManagerCustomizer) throws Exception {
-        Assertions.assertNull(connManager);
-        Assertions.assertNull(client);
-
-        final PoolingAsyncClientConnectionManagerBuilder connManagerBuilder = PoolingAsyncClientConnectionManagerBuilder.create();
-        connManagerBuilder.setTlsStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
-        connManagerBuilder.setDefaultConnectionConfig(ConnectionConfig.custom()
-                .setSocketTimeout(timeout)
-                .setConnectTimeout(timeout)
-                .build());
-        connManagerCustomizer.accept(connManagerBuilder);
-
-        connManager = connManagerBuilder.build();
-
-        final MinimalHttpAsyncClient minimal = HttpAsyncClients.createMinimal(
-                h2Config,
-                http1Config,
-                IOReactorConfig.custom()
-                        .setSoTimeout(timeout)
-                        .build(),
-                connManager);
-        client = minimal;
-        client.start();
-        return minimal;
-    }
-
-    public MinimalH2AsyncClient startMinimalH2Client(final H2Config h2Config) throws Exception {
-        Assertions.assertNull(client);
-
-        final MinimalH2AsyncClient minimal = HttpAsyncClients.createHttp2Minimal(
-                h2Config,
-                IOReactorConfig.custom()
-                        .setSoTimeout(timeout)
-                        .build(),
-                new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
-        client = minimal;
-        client.start();
-        return minimal;
+    public TestAsyncClient client() throws Exception {
+        if (client == null) {
+            client = clientBuilder.build();
+        }
+        return client;
     }
 
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncServer.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncServer.java
new file mode 100644
index 000000000..27c134e87
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncServer.java
@@ -0,0 +1,101 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import java.net.InetSocketAddress;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorStatus;
+import org.apache.hc.core5.reactor.ListenerEndpoint;
+import org.apache.hc.core5.testing.nio.H2TestServer;
+import org.apache.hc.core5.util.TimeValue;
+
+public class TestAsyncServer {
+
+    private final H2TestServer server;
+    private final H2Config h2Config;
+    private final Http1Config http1Config;
+    private final HttpProcessor httpProcessor;
+    private final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator;
+
+    TestAsyncServer(
+            final H2TestServer server,
+            final H2Config h2Config,
+            final Http1Config http1Config,
+            final HttpProcessor httpProcessor,
+            final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) {
+        this.server = server;
+        this.h2Config = h2Config;
+        this.http1Config = http1Config;
+        this.httpProcessor = httpProcessor;
+        this.exchangeHandlerDecorator = exchangeHandlerDecorator;
+    }
+
+    public Future<ListenerEndpoint> listen(final InetSocketAddress address) {
+        return server.listen(address);
+    }
+
+    public Set<ListenerEndpoint> getEndpoints() {
+        return server.getEndpoints();
+    }
+
+    public IOReactorStatus getStatus() {
+        return server.getStatus();
+    }
+
+    public void awaitShutdown(final TimeValue waitTime) throws InterruptedException {
+        server.awaitShutdown(waitTime);
+    }
+
+    public void initiateShutdown() {
+        server.initiateShutdown();
+    }
+
+    public void shutdown(final TimeValue graceTime) {
+        server.shutdown(graceTime);
+    }
+
+    public void close() throws Exception {
+        server.close();
+    }
+
+    public InetSocketAddress start() throws Exception {
+        if (http1Config == null) {
+            return server.start(httpProcessor, exchangeHandlerDecorator, h2Config);
+        } else {
+            return server.start(httpProcessor, exchangeHandlerDecorator, http1Config);
+        }
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncServerBootstrap.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncServerBootstrap.java
new file mode 100644
index 000000000..2e518112f
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/extension/TestAsyncServerBootstrap.java
@@ -0,0 +1,133 @@
+/*
+ * ====================================================================
+ * 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.async.extension;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.AsyncServerRequestHandler;
+import org.apache.hc.core5.http.nio.support.BasicServerExchangeHandler;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.testing.nio.H2TestServer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+
+public class TestAsyncServerBootstrap {
+
+    static final class HandlerEntry<T> {
+
+        final String hostname;
+        final String uriPattern;
+        final T handler;
+
+        public HandlerEntry(final String hostname, final String uriPattern, final T handler) {
+            this.hostname = hostname;
+            this.uriPattern = uriPattern;
+            this.handler = handler;
+        }
+
+        public HandlerEntry(final String uriPattern, final T handler) {
+            this(null, uriPattern, handler);
+        }
+
+    }
+
+    private final URIScheme scheme;
+    private final ServerProtocolLevel serverProtocolLevel;
+
+    private final List<HandlerEntry<Supplier<AsyncServerExchangeHandler>>> handlerList;
+    private Timeout timeout;
+    private HttpProcessor httpProcessor;
+    private Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator;
+
+    public TestAsyncServerBootstrap(final URIScheme scheme, final ServerProtocolLevel serverProtocolLevel) {
+        this.scheme = scheme != null ? scheme : URIScheme.HTTP;
+        this.serverProtocolLevel = serverProtocolLevel != null ? serverProtocolLevel : ServerProtocolLevel.STANDARD;
+        this.handlerList = new ArrayList<>();
+    }
+
+    public ServerProtocolLevel getProtocolLevel() {
+        return serverProtocolLevel;
+    }
+
+    public TestAsyncServerBootstrap register(final String uriPattern, final Supplier<AsyncServerExchangeHandler> supplier) {
+        Args.notNull(uriPattern, "URI pattern");
+        Args.notNull(supplier, "Exchange handler supplier");
+        handlerList.add(new HandlerEntry<>(uriPattern, supplier));
+        return this;
+    }
+
+    public TestAsyncServerBootstrap register(
+            final String uriPattern,
+            final AsyncServerRequestHandler<AsyncServerExchangeHandler> requestHandler) {
+        return register(uriPattern, () -> new BasicServerExchangeHandler<>(requestHandler));
+    }
+
+    public TestAsyncServerBootstrap setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    public TestAsyncServerBootstrap setHttpProcessor(final HttpProcessor httpProcessor) {
+        this.httpProcessor = httpProcessor;
+        return this;
+    }
+
+    public TestAsyncServerBootstrap setExchangeHandlerDecorator(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) {
+        this.exchangeHandlerDecorator = exchangeHandlerDecorator;
+        return this;
+    }
+
+    public TestAsyncServer build() throws Exception {
+        final H2TestServer server = new H2TestServer(
+                IOReactorConfig.custom()
+                        .setSoTimeout(timeout)
+                        .build(),
+                scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null,
+                null,
+                null);
+        for (final HandlerEntry<Supplier<AsyncServerExchangeHandler>> entry: handlerList) {
+            server.register(entry.uriPattern, entry.handler);
+        }
+        return new TestAsyncServer(
+                server,
+                serverProtocolLevel == ServerProtocolLevel.H2_ONLY ? H2Config.DEFAULT : null,
+                serverProtocolLevel == ServerProtocolLevel.STANDARD ? Http1Config.DEFAULT : null,
+                httpProcessor,
+                exchangeHandlerDecorator);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/fluent/TestFluent.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/fluent/TestFluent.java
index cb3378f41..c97aefbde 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/fluent/TestFluent.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/fluent/TestFluent.java
@@ -27,6 +27,7 @@
 package org.apache.hc.client5.testing.fluent;
 
 import java.io.File;
+import java.net.InetSocketAddress;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 
@@ -34,7 +35,9 @@ import org.apache.hc.client5.http.ClientProtocolException;
 import org.apache.hc.client5.http.HttpResponseException;
 import org.apache.hc.client5.http.fluent.Content;
 import org.apache.hc.client5.http.fluent.Request;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
 import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.TestServer;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpHost;
@@ -42,7 +45,6 @@ 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.entity.StringEntity;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
 import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
@@ -54,52 +56,51 @@ public class TestFluent {
     public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
 
     @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
-
-    public HttpHost targetHost() {
-        return testResources.targetHost();
-    }
+    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, ClientProtocolLevel.STANDARD, TIMEOUT);
 
     @BeforeEach
     public void setUp() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("/", (request, response, context) ->
-                response.setEntity(new StringEntity("All is well", ContentType.TEXT_PLAIN)));
-        server.registerHandler("/echo", (request, response, context) -> {
-            HttpEntity responseEntity = null;
-            final HttpEntity requestEntity = request.getEntity();
-            if (requestEntity != null) {
-                final String contentTypeStr = requestEntity.getContentType();
-                final ContentType contentType = contentTypeStr == null ? ContentType.DEFAULT_TEXT : ContentType.parse(contentTypeStr);
-                if (ContentType.TEXT_PLAIN.getMimeType().equals(contentType.getMimeType())) {
-                    responseEntity = new StringEntity(
-                            EntityUtils.toString(requestEntity), ContentType.TEXT_PLAIN);
-                }
-            }
-            if (responseEntity == null) {
-                responseEntity = new StringEntity("echo", ContentType.TEXT_PLAIN);
-            }
-            response.setEntity(responseEntity);
-        });
-
-        // Handler for large content large message
-        server.registerHandler("/large-message", (request, response, context) -> {
-            final String largeContent = generateLargeString(10000); // Large content string
-            response.setEntity(new StringEntity(largeContent, ContentType.TEXT_PLAIN));
-        });
-
-        // Handler for large content large message with error
-        server.registerHandler("/large-message-error", (request, response, context) -> {
-            final String largeContent = generateLargeString(10000); // Large content string
-            response.setCode(HttpStatus.SC_REDIRECTION);
-            response.setEntity(new StringEntity(largeContent, ContentType.TEXT_PLAIN));
-        });
+        testResources.configureServer(bootstrap -> bootstrap
+                .register("/", (request, response, context) ->
+                        response.setEntity(new StringEntity("All is well", ContentType.TEXT_PLAIN)))
+                .register("/echo", (request, response, context) -> {
+                    HttpEntity responseEntity = null;
+                    final HttpEntity requestEntity = request.getEntity();
+                    if (requestEntity != null) {
+                        final String contentTypeStr = requestEntity.getContentType();
+                        final ContentType contentType = contentTypeStr == null ? ContentType.DEFAULT_TEXT : ContentType.parse(contentTypeStr);
+                        if (ContentType.TEXT_PLAIN.getMimeType().equals(contentType.getMimeType())) {
+                            responseEntity = new StringEntity(
+                                    EntityUtils.toString(requestEntity), ContentType.TEXT_PLAIN);
+                        }
+                    }
+                    if (responseEntity == null) {
+                        responseEntity = new StringEntity("echo", ContentType.TEXT_PLAIN);
+                    }
+                    response.setEntity(responseEntity);
+                })
+                // Handler for large content large message
+                .register("/large-message", (request, response, context) -> {
+                    final String largeContent = generateLargeString(10000); // Large content string
+                    response.setEntity(new StringEntity(largeContent, ContentType.TEXT_PLAIN));
+                })
+                // Handler for large content large message with error
+                .register("/large-message-error", (request, response, context) -> {
+                    final String largeContent = generateLargeString(10000); // Large content string
+                    response.setCode(HttpStatus.SC_REDIRECTION);
+                    response.setEntity(new StringEntity(largeContent, ContentType.TEXT_PLAIN));
+                }));
+    }
 
+    public HttpHost startServer() throws Exception {
+        final TestServer server = testResources.server();
+        final InetSocketAddress inetSocketAddress = server.start();
+        return new HttpHost(testResources.scheme().id, "localhost", inetSocketAddress.getPort());
     }
 
     @Test
     public void testGetRequest() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         final String message = Request.get(baseURL + "/").execute().returnContent().asString();
         Assertions.assertEquals("All is well", message);
@@ -107,7 +108,7 @@ public class TestFluent {
 
     @Test
     public void testGetRequestByName() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         final String message = Request.create("GET", baseURL + "/").execute().returnContent().asString();
         Assertions.assertEquals("All is well", message);
@@ -115,7 +116,7 @@ public class TestFluent {
 
     @Test
     public void testGetRequestByNameWithURI() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         final String message = Request.create("GET", new URI(baseURL + "/")).execute().returnContent().asString();
         Assertions.assertEquals("All is well", message);
@@ -123,7 +124,7 @@ public class TestFluent {
 
     @Test
     public void testGetRequestFailure() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         Assertions.assertThrows(ClientProtocolException.class, () ->
                 Request.get(baseURL + "/boom").execute().returnContent().asString());
@@ -131,7 +132,7 @@ public class TestFluent {
 
     @Test
     public void testPostRequest() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         final String message1 = Request.post(baseURL + "/echo")
                 .bodyString("what is up?", ContentType.TEXT_PLAIN)
@@ -145,7 +146,7 @@ public class TestFluent {
 
     @Test
     public void testContentAsStringWithCharset() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         final Content content = Request.post(baseURL + "/echo").bodyByteArray("Ü".getBytes(StandardCharsets.UTF_8)).execute()
                 .returnContent();
@@ -156,7 +157,7 @@ public class TestFluent {
 
     @Test
     public void testConnectionRelease() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
         for (int i = 0; i < 20; i++) {
             Request.get(baseURL + "/").execute().returnContent();
@@ -183,7 +184,7 @@ public class TestFluent {
     @Test
     public void testLargeResponse() throws Exception {
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
 
         final Content content = Request.get(baseURL + "/large-message").execute().returnContent();
@@ -192,7 +193,7 @@ public class TestFluent {
 
     @Test
     public void testLargeResponseError() throws Exception {
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
         final String baseURL = "http://localhost:" + target.getPort();
 
         try {
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java
new file mode 100644
index 000000000..85bf42774
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * 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.sync;
+
+import java.net.InetSocketAddress;
+import java.util.function.Consumer;
+
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
+import org.apache.hc.client5.testing.sync.extension.TestClientBuilder;
+import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.TestServer;
+import org.apache.hc.client5.testing.sync.extension.TestServerBootstrap;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public abstract class AbstractIntegrationTestBase {
+
+    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
+
+    @RegisterExtension
+    private TestClientResources testResources;
+
+    protected AbstractIntegrationTestBase(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel) {
+        this.testResources = new TestClientResources(scheme, clientProtocolLevel, TIMEOUT);
+    }
+
+    public URIScheme scheme() {
+        return testResources.scheme();
+    }
+
+    public ClientProtocolLevel getClientProtocolLevel() {
+        return testResources.getClientProtocolLevel();
+    }
+
+    public void configureServer(final Consumer<TestServerBootstrap> serverCustomizer) {
+        testResources.configureServer(serverCustomizer);
+    }
+
+    public HttpHost startServer() throws Exception {
+        final TestServer server = testResources.server();
+        final InetSocketAddress inetSocketAddress = server.start();
+        return new HttpHost(testResources.scheme().id, "localhost", inetSocketAddress.getPort());
+    }
+
+    public void configureClient(final Consumer<TestClientBuilder> clientCustomizer) {
+        testResources.configureClient(clientCustomizer);
+    }
+
+    public TestClient client() throws Exception {
+        return testResources.client();
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestBasicConnectionManager.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestBasicConnectionManager.java
index 1d29bc8e0..3c7b13e04 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestBasicConnectionManager.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestBasicConnectionManager.java
@@ -27,35 +27,31 @@
 package org.apache.hc.client5.testing.sync;
 
 import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
 import org.apache.hc.client5.testing.classic.RandomHandler;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
-public class TestBasicConnectionManager {
+public class TestBasicConnectionManager extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
+    public TestBasicConnectionManager() {
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+    }
 
     @Test
     public void testBasics() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = testResources.startClient(builder -> builder
-                .setConnectionManager(new BasicHttpClientConnectionManager())
-        );
+        configureClient(builder -> builder
+                .setConnectionManager(new BasicHttpClientConnectionManager()));
+        final TestClient client = client();
 
         final HttpGet get = new HttpGet("/random/1024");
         client.execute(target, get, response -> {
@@ -67,13 +63,13 @@ public class TestBasicConnectionManager {
 
     @Test
     public void testConnectionStillInUse() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = testResources.startClient(builder -> builder
-                .setConnectionManager(new BasicHttpClientConnectionManager())
-        );
+        configureClient(builder -> builder
+                .setConnectionManager(new BasicHttpClientConnectionManager()));
+        final TestClient client = client();
 
         final HttpGet get1 = new HttpGet("/random/1024");
         client.executeOpen(target, get1, null);
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
index 97596a7f1..c52677eab 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
@@ -58,15 +58,15 @@ import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
-import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.BasicTestAuthenticator;
 import org.apache.hc.client5.testing.auth.Authenticator;
 import org.apache.hc.client5.testing.auth.BearerAuthenticationHandler;
 import org.apache.hc.client5.testing.classic.AuthenticatingDecorator;
 import org.apache.hc.client5.testing.classic.EchoHandler;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
+import org.apache.hc.client5.testing.sync.extension.TestServerBootstrap;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.HeaderElements;
@@ -77,10 +77,8 @@ import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
 import org.apache.hc.core5.http.config.Registry;
 import org.apache.hc.core5.http.config.RegistryBuilder;
-import org.apache.hc.core5.http.impl.HttpProcessors;
 import org.apache.hc.core5.http.io.HttpRequestHandler;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
@@ -88,62 +86,42 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.net.URIAuthority;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 import org.mockito.Mockito;
 
 /**
  * Unit tests for automatic client authentication.
  */
-public abstract class TestClientAuthentication {
-
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources;
+public abstract class TestClientAuthentication extends AbstractIntegrationTestBase {
 
     protected TestClientAuthentication(final URIScheme scheme) {
-        this.testResources = new TestClientResources(scheme, TIMEOUT);
-    }
-
-    public URIScheme scheme() {
-        return testResources.scheme();
-    }
-
-    public ClassicTestServer startServer(final Authenticator authenticator) throws IOException {
-        return testResources.startServer(
-                null,
-                null,
-                requestHandler -> new AuthenticatingDecorator(requestHandler, authenticator));
-    }
-
-    public ClassicTestServer startServer() throws IOException {
-        return startServer(new BasicTestAuthenticator("test:test", "test realm"));
-    }
-
-    public CloseableHttpClient startClient(final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startClient(clientCustomizer);
+        super(scheme, ClientProtocolLevel.STANDARD);
     }
 
-    public CloseableHttpClient startClient() throws Exception {
-        return testResources.startClient(builder -> {});
+    public void configureServerWithBasicAuth(final Authenticator authenticator,
+                                             final Consumer<TestServerBootstrap> serverCustomizer) throws IOException {
+        configureServer(bootstrap -> {
+            bootstrap.setExchangeHandlerDecorator(requestHandler ->
+                    new AuthenticatingDecorator(requestHandler, authenticator));
+            serverCustomizer.accept(bootstrap);
+        });
     }
 
-    public HttpHost targetHost() {
-        return testResources.targetHost();
+    public void configureServerWithBasicAuth(final Consumer<TestServerBootstrap> serverCustomizer) throws IOException {
+        configureServerWithBasicAuth(
+                new BasicTestAuthenticator("test:test", "test realm"),
+                serverCustomizer);
     }
 
     @Test
     public void testBasicAuthenticationNoCreds() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
@@ -163,11 +141,11 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationFailure() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
@@ -189,11 +167,11 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationSuccess() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpGet httpget = new HttpGet("/");
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
@@ -214,11 +192,11 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationSuccessOnNonRepeatablePutExpectContinue() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final RequestConfig config = RequestConfig.custom()
                 .setExpectContinueEnabled(true)
@@ -227,8 +205,8 @@ public abstract class TestClientAuthentication {
         httpput.setConfig(config);
         httpput.setEntity(new InputStreamEntity(
                 new ByteArrayInputStream(
-                        new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ),
-                        -1, null));
+                        new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9}),
+                -1, null));
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
@@ -245,19 +223,19 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationFailureOnNonRepeatablePutDontExpectContinue() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final RequestConfig config = RequestConfig.custom().setExpectContinueEnabled(false).build();
         final HttpPut httpput = new HttpPut("/");
         httpput.setConfig(config);
         httpput.setEntity(new InputStreamEntity(
                 new ByteArrayInputStream(
-                        new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ),
-                        -1, null));
+                        new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9}),
+                -1, null));
 
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
@@ -276,11 +254,11 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationSuccessOnRepeatablePost() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpPost httppost = new HttpPost("/");
         httppost.setEntity(new StringEntity("some important stuff", StandardCharsets.US_ASCII));
@@ -304,16 +282,16 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationFailureOnNonRepeatablePost() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpPost httppost = new HttpPost("/");
         httppost.setEntity(new InputStreamEntity(
                 new ByteArrayInputStream(
-                        new byte[] { 0,1,2,3,4,5,6,7,8,9 }), -1, null));
+                        new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}), -1, null));
 
         final HttpClientContext context = HttpClientContext.create();
         context.setRequestConfig(RequestConfig.custom()
@@ -335,17 +313,18 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationCredentialsCaching() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
         final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
 
-        final CloseableHttpClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setTargetAuthenticationStrategy(authStrategy)
                 .addResponseInterceptorLast((response, entity, context)
                         -> responseQueue.add(BasicResponseBuilder.copy(response).build())));
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         context.setCredentialsProvider(CredentialsProviderBuilder.create()
@@ -372,17 +351,18 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
         final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
 
-        final CloseableHttpClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setTargetAuthenticationStrategy(authStrategy)
                 .addResponseInterceptorLast((response, entity, context)
                         -> responseQueue.add(BasicResponseBuilder.copy(response).build())));
+        final TestClient client = client();
 
         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
                 .add(target, "test", "test".toCharArray())
@@ -393,7 +373,7 @@ public abstract class TestClientAuthentication {
         context.setAuthCache(authCache);
         context.setCredentialsProvider(credentialsProvider);
 
-        for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
+        for (final String requestPath : new String[]{"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
             final HttpGet httpget = new HttpGet(requestPath);
             client.execute(target, httpget, context, response -> {
                 final HttpEntity entity1 = response.getEntity();
@@ -415,7 +395,7 @@ public abstract class TestClientAuthentication {
         authCache.clear();
         Mockito.reset(authStrategy);
 
-        for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/", "/buh/a"}) {
+        for (final String requestPath : new String[]{"/blah/a", "/yada/a", "/blah/blah/", "/buh/a"}) {
             final HttpGet httpget = new HttpGet(requestPath);
             client.execute(target, httpget, context, response -> {
                 final HttpEntity entity1 = response.getEntity();
@@ -436,7 +416,7 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testAuthenticationCredentialsCachingReAuthenticationOnDifferentRealm() throws Exception {
-        final ClassicTestServer server = startServer(new Authenticator() {
+        configureServerWithBasicAuth(new Authenticator() {
 
             @Override
             public boolean authenticate(final URIAuthority authority, final String requestUri, final String credentials) {
@@ -460,15 +440,15 @@ public abstract class TestClientAuthentication {
                 }
             }
 
-        });
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        }, bootstrap -> bootstrap.register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
 
-        final CloseableHttpClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setTargetAuthenticationStrategy(authStrategy)
         );
+        final TestClient client = client();
 
         final CredentialsProvider credsProvider = CredentialsProviderBuilder.create()
                 .add(new AuthScope(target, "this realm", null), "test", "this".toCharArray())
@@ -513,12 +493,12 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testAuthenticationUserinfoInRequest() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
-        final HttpGet httpget = new HttpGet("http://test:test@" +  target.toHostString() + "/");
+        final TestClient client = client();
+        final HttpGet httpget = new HttpGet("http://test:test@" + target.toHostString() + "/");
 
         final HttpClientContext context = HttpClientContext.create();
         Assertions.assertThrows(ClientProtocolException.class, () -> client.execute(target, httpget, context, response -> null));
@@ -527,11 +507,12 @@ public abstract class TestClientAuthentication {
     @Test
     public void testPreemptiveAuthentication() throws Exception {
         final Authenticator authenticator = Mockito.spy(new BasicTestAuthenticator("test:test", "test realm"));
-        final ClassicTestServer server = startServer(authenticator);
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(authenticator,
+                bootstrap -> bootstrap
+                        .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final BasicScheme basicScheme = new BasicScheme();
         basicScheme.initPreemptive(new UsernamePasswordCredentials("test", "test".toCharArray()));
@@ -555,11 +536,12 @@ public abstract class TestClientAuthentication {
     @Test
     public void testPreemptiveAuthenticationFailure() throws Exception {
         final Authenticator authenticator = Mockito.spy(new BasicTestAuthenticator("test:test", "test realm"));
-        final ClassicTestServer server = startServer(authenticator);
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServerWithBasicAuth(authenticator,
+                bootstrap -> bootstrap
+                        .register("*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final AuthCache authCache = new BasicAuthCache();
@@ -602,11 +584,11 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testAuthenticationTargetAsProxy() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("*", new ProxyAuthHandler());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("*", new ProxyAuthHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = testResources.startClient(builder -> {});
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
@@ -623,22 +605,22 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testConnectionCloseAfterAuthenticationSuccess() throws Exception {
-        final ClassicTestServer server = testResources.startServer(
-                Http1Config.DEFAULT,
-                HttpProcessors.server(),
-                requestHandler -> new AuthenticatingDecorator(requestHandler, new BasicTestAuthenticator("test:test", "test realm")) {
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler ->
+                        new AuthenticatingDecorator(requestHandler, new BasicTestAuthenticator("test:test", "test realm")) {
 
-                    @Override
-                    protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
-                        unauthorized.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE);
-                    }
+                            @Override
+                            protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
+                                unauthorized.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE);
+                            }
 
-                }
-        );
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+                        }
+                )
+                .register("*", new EchoHandler()));
 
-        final CloseableHttpClient client = startClient();
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = CredentialsProviderBuilder.create()
@@ -701,28 +683,29 @@ public abstract class TestClientAuthentication {
             }
         };
 
-        final ClassicTestServer server = testResources.startServer(
-                Http1Config.DEFAULT,
-                HttpProcessors.server(),
-                requestHandler -> new AuthenticatingDecorator(requestHandler, authenticator) {
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler ->
+                        new AuthenticatingDecorator(requestHandler, authenticator) {
 
-                    @Override
-                    protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
-                        unauthorized.removeHeaders(HttpHeaders.WWW_AUTHENTICATE);
-                        unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, "MyBasic realm=\"test realm\"");
-                    }
+                            @Override
+                            protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
+                                unauthorized.removeHeaders(HttpHeaders.WWW_AUTHENTICATE);
+                                unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, "MyBasic realm=\"test realm\"");
+                            }
 
-                }
-        );
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+                        }
+                )
+                .register("*", new EchoHandler()));
 
-        final CloseableHttpClient client = startClient(builder -> builder
+        final HttpHost target = startServer();
+
+        configureClient(builder -> builder
                 .setDefaultAuthSchemeRegistry(authSchemeRegistry)
-                .setDefaultCredentialsProvider(credsProvider)
         );
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
+        context.setCredentialsProvider(credsProvider);
         for (int i = 0; i < 10; i++) {
             final HttpGet httpget = new HttpGet("/");
             httpget.setConfig(config);
@@ -738,22 +721,22 @@ public abstract class TestClientAuthentication {
 
     @Test
     public void testAuthenticationFallback() throws Exception {
-        final ClassicTestServer server = testResources.startServer(
-                Http1Config.DEFAULT,
-                HttpProcessors.server(),
-                requestHandler -> new AuthenticatingDecorator(requestHandler, new BasicTestAuthenticator("test:test", "test realm")) {
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler ->
+                        new AuthenticatingDecorator(requestHandler, new BasicTestAuthenticator("test:test", "test realm")) {
 
-                    @Override
-                    protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
-                        unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"test realm\" invalid");
-                    }
+                            @Override
+                            protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
+                                unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"test realm\" invalid");
+                            }
 
-                }
-        );
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
+                        }
+                )
+                .register("*", new EchoHandler()));
+
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
@@ -784,17 +767,18 @@ public abstract class TestClientAuthentication {
             buf.append(CHARS.charAt(secureRandom.nextInt(CHARS.length() - 1)));
         }
         final String token = buf.toString();
-        final ClassicTestServer server = testResources.startServer(
-                Http1Config.DEFAULT,
-                HttpProcessors.server(),
-                requestHandler -> new AuthenticatingDecorator(
-                        requestHandler,
-                        new BearerAuthenticationHandler(),
-                        new BasicTestAuthenticator(token, "test realm")));
-        server.registerHandler("*", new EchoHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler ->
+                        new AuthenticatingDecorator(
+                                requestHandler,
+                                new BearerAuthenticationHandler(),
+                                new BasicTestAuthenticator(token, "test realm"))
+                )
+                .register("*", new EchoHandler()));
+
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
 
         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
 
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
index 537fd9c54..57bf594d2 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
@@ -33,17 +33,16 @@ import java.util.Random;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
 
 import org.apache.hc.client5.http.HttpRequestRetryStrategy;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.classic.methods.HttpPost;
-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.io.PoolingHttpClientConnectionManager;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.utils.URIUtils;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.Header;
@@ -64,46 +63,18 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.net.URIBuilder;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
 import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * Client protocol handling tests.
  */
-public abstract class TestClientRequestExecution {
+public abstract class TestClientRequestExecution extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources;
-
-    protected TestClientRequestExecution(final URIScheme scheme) {
-        this.testResources = new TestClientResources(scheme, TIMEOUT);
-    }
-
-    public URIScheme scheme() {
-        return testResources.scheme();
-    }
-
-    public ClassicTestServer startServer() throws IOException {
-        return testResources.startServer(null, null, null);
-    }
-
-    public CloseableHttpClient startClient(final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startClient(clientCustomizer);
-    }
-
-    public CloseableHttpClient startClient() throws Exception {
-        return testResources.startClient(builder -> {});
-    }
-
-    public HttpHost targetHost() {
-        return testResources.targetHost();
+    public TestClientRequestExecution(final URIScheme scheme) {
+        super(scheme, ClientProtocolLevel.STANDARD);
     }
 
     private static class SimpleService implements HttpRequestHandler {
@@ -161,9 +132,8 @@ public abstract class TestClientRequestExecution {
 
     @Test
     public void testAutoGeneratedHeaders() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
         final HttpRequestInterceptor interceptor = (request, entityDetails, context) -> request.addHeader("my-header", "stuff");
 
@@ -196,11 +166,12 @@ public abstract class TestClientRequestExecution {
 
         };
 
-        final CloseableHttpClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .addRequestInterceptorFirst(interceptor)
                 .setRequestExecutor(new FaultyHttpRequestExecutor("Oppsie"))
                 .setRetryStrategy(requestRetryStrategy)
         );
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
 
@@ -221,9 +192,8 @@ public abstract class TestClientRequestExecution {
 
     @Test
     public void testNonRepeatableEntity() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
         final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
 
@@ -254,10 +224,11 @@ public abstract class TestClientRequestExecution {
 
         };
 
-        final CloseableHttpClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setRequestExecutor(new FaultyHttpRequestExecutor("a message showing that this failed"))
                 .setRetryStrategy(requestRetryStrategy)
         );
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
 
@@ -272,11 +243,10 @@ public abstract class TestClientRequestExecution {
 
     @Test
     public void testNonCompliantURI() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         final ClassicHttpRequest request = new BasicClassicHttpRequest("GET", "{{|boom|}}");
@@ -293,11 +263,10 @@ public abstract class TestClientRequestExecution {
 
     @Test
     public void testRelativeRequestURIWithFragment() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet httpget = new HttpGet("/stuff#blahblah");
         final HttpClientContext context = HttpClientContext.create();
@@ -314,11 +283,10 @@ public abstract class TestClientRequestExecution {
 
     @Test
     public void testAbsoluteRequestURIWithFragment() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final URI uri = new URIBuilder()
             .setHost(target.getHostName())
@@ -347,13 +315,12 @@ public abstract class TestClientRequestExecution {
     @Test @Disabled("Fails intermittently with GitHub Actions")
     public void testRequestCancellation() throws Exception {
         startServer();
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnPerRoute(1)
-                        .setMaxConnTotal(1),
-                builder -> {});
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+        connManager.setMaxTotal(1);
+        connManager.setDefaultMaxPerRoute(1);
 
         final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
         try {
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java
index 8d366d3a6..d77ca68ca 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java
@@ -32,13 +32,14 @@ import java.util.concurrent.TimeoutException;
 
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.config.ConnectionConfig;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
 import org.apache.hc.client5.http.io.ConnectionEndpoint;
 import org.apache.hc.client5.http.io.LeaseRequest;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.classic.RandomHandler;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.HttpException;
@@ -56,24 +57,21 @@ import org.apache.hc.core5.http.protocol.RequestContent;
 import org.apache.hc.core5.http.protocol.RequestTargetHost;
 import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
 import org.apache.hc.core5.pool.PoolReusePolicy;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
 import org.apache.hc.core5.util.TimeValue;
 import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * Tests for {@code PoolingHttpClientConnectionManager} that do require a server
  * to communicate with.
  */
-public class TestConnectionManagement {
+public class TestConnectionManagement extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
+    public TestConnectionManagement() {
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+    }
 
     ConnectionEndpoint.RequestExecutor exec;
 
@@ -97,30 +95,17 @@ public class TestConnectionManagement {
         };
     }
 
-    public ClassicTestServer startServer() throws IOException {
-        return testResources.startServer(null, null, null);
-    }
-
-    public CloseableHttpClient startClient() throws Exception {
-        return testResources.startClient(b -> {}, b -> {});
-    }
-
-    public HttpHost targetHost() {
-        return testResources.targetHost();
-    }
-
     /**
      * Tests releasing and re-using a connection after a response is read.
      */
     @Test
     public void testReleaseConnection() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
         connManager.setMaxTotal(1);
 
         final HttpRoute route = new HttpRoute(target, null, false);
@@ -178,13 +163,12 @@ public class TestConnectionManagement {
      */
     @Test
     public void testReleaseConnectionWithTimeLimits() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        startClient();
-
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
         connManager.setMaxTotal(1);
 
         final HttpRoute route = new HttpRoute(target, null, false);
@@ -250,11 +234,9 @@ public class TestConnectionManagement {
 
     @Test
     public void testCloseExpiredIdleConnections() throws Exception {
-        startServer();
-        final HttpHost target = targetHost();
-        startClient();
-
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final HttpHost target = startServer();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
         connManager.setMaxTotal(1);
 
         final HttpRoute route = new HttpRoute(target, null, false);
@@ -292,22 +274,21 @@ public class TestConnectionManagement {
 
     @Test
     public void testCloseExpiredTTLConnections() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        testResources.startClient(
-                builder -> builder
+        configureClient(builder -> builder
+                .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
                         .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
                         .setConnPoolPolicy(PoolReusePolicy.LIFO)
                         .setDefaultConnectionConfig(ConnectionConfig.custom()
                                 .setTimeToLive(TimeValue.ofMilliseconds(100))
                                 .build())
-                        .setMaxConnTotal(1),
-                builder -> {}
-        );
+                        .build()));
+        final TestClient client = client();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
         connManager.setMaxTotal(1);
 
         final HttpRoute route = new HttpRoute(target, null, false);
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionReuse.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionReuse.java
index 6029d2574..37017a0f2 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionReuse.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionReuse.java
@@ -39,7 +39,8 @@ import org.apache.hc.client5.http.classic.methods.HttpPost;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
 import org.apache.hc.client5.testing.classic.RandomHandler;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.EntityDetails;
@@ -55,39 +56,25 @@ import org.apache.hc.core5.http.impl.HttpProcessors;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.http.protocol.HttpProcessor;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
-public class TestConnectionReuse {
+public class TestConnectionReuse extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
-
-    public HttpHost targetHost() {
-        return testResources.targetHost();
+    public TestConnectionReuse() {
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
     }
 
     @Test
     public void testReuseOfPersistentConnections() throws Exception {
-        final ClassicTestServer server =  testResources.startServer(  null, null, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(5)
-                        .setMaxConnPerRoute(5),
-                builder -> {
-                }
-        );
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+        connManager.setMaxTotal(5);
+        connManager.setDefaultMaxPerRoute(5);
 
         final WorkerThread[] workers = new WorkerThread[10];
         for (int i = 0; i < workers.length; i++) {
@@ -117,19 +104,14 @@ public class TestConnectionReuse {
 
     @Test
     public void testReuseOfPersistentConnectionsWithStreamedRequestAndResponse() throws Exception {
-        final ClassicTestServer server =  testResources.startServer(  null, null, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(5)
-                        .setMaxConnPerRoute(5),
-                builder -> {
-                }
-        );
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+        connManager.setMaxTotal(5);
+        connManager.setDefaultMaxPerRoute(5);
 
         final WorkerThread[] workers = new WorkerThread[10];
         for (int i = 0; i < workers.length; i++) {
@@ -176,21 +158,16 @@ public class TestConnectionReuse {
 
     @Test
     public void testReuseOfClosedConnections() throws Exception {
-        final HttpProcessor httpproc = HttpProcessors.customServer(null)
-                .add(new AlwaysCloseConn())
-                .build();
-        final ClassicTestServer server =  testResources.startServer(  null, httpproc, null);
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(5)
-                        .setMaxConnPerRoute(5),
-                builder -> {
-                }
-        );
+        configureServer(bootstrap -> bootstrap
+                .setHttpProcessor(HttpProcessors.customServer(null)
+                        .add(new AlwaysCloseConn())
+                        .build()));
+        final HttpHost target = startServer();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+        connManager.setMaxTotal(5);
+        connManager.setDefaultMaxPerRoute(5);
 
         final WorkerThread[] workers = new WorkerThread[10];
         for (int i = 0; i < workers.length; i++) {
@@ -220,19 +197,14 @@ public class TestConnectionReuse {
 
     @Test
     public void testReuseOfAbortedConnections() throws Exception {
-        final ClassicTestServer server =  testResources.startServer(  null, null, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(5)
-                        .setMaxConnPerRoute(5),
-                builder -> {
-                }
-        );
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+        connManager.setMaxTotal(5);
+        connManager.setDefaultMaxPerRoute(5);
 
         final WorkerThread[] workers = new WorkerThread[10];
         for (int i = 0; i < workers.length; i++) {
@@ -262,23 +234,17 @@ public class TestConnectionReuse {
 
     @Test
     public void testKeepAliveHeaderRespected() throws Exception {
-        final HttpProcessor httpproc = HttpProcessors.customServer(null)
-                .add(new ResponseKeepAlive())
-                .build();
-        final ClassicTestServer server =  testResources.startServer(  null, httpproc, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(1)
-                        .setMaxConnPerRoute(1),
-                builder -> {
-                }
-        );
-
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
-
+        configureServer(bootstrap -> bootstrap
+                .setHttpProcessor(HttpProcessors.customServer(null)
+                        .add(new ResponseKeepAlive())
+                        .build())
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+        connManager.setMaxTotal(1);
+        connManager.setDefaultMaxPerRoute(1);
 
         client.execute(target, new HttpGet("/random/2000"), response -> {
             EntityUtils.consume(response.getEntity());
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestContentCodings.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestContentCodings.java
index 211ce9b08..3127b245f 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestContentCodings.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestContentCodings.java
@@ -38,16 +38,15 @@ import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.function.Consumer;
 import java.util.zip.Deflater;
 import java.util.zip.GZIPOutputStream;
 
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler;
 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.io.PoolingHttpClientConnectionManager;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.HeaderElement;
@@ -61,46 +60,18 @@ import org.apache.hc.core5.http.io.entity.InputStreamEntity;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.MessageSupport;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * Test case for how Content Codings are processed. By default, we want to do the right thing and
  * require no intervention from the user of HttpClient, but we still want to let clients do their
  * own thing if they so wish.
  */
-public abstract class TestContentCodings {
-
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources;
+public abstract class TestContentCodings extends AbstractIntegrationTestBase {
 
     protected TestContentCodings(final URIScheme scheme) {
-        this.testResources = new TestClientResources(scheme, TIMEOUT);
-    }
-
-    public URIScheme scheme() {
-        return testResources.scheme();
-    }
-
-    public ClassicTestServer startServer() throws IOException {
-        return testResources.startServer(null, null, null);
-    }
-
-    public CloseableHttpClient startClient(final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startClient(clientCustomizer);
-    }
-
-    public CloseableHttpClient startClient() throws Exception {
-        return testResources.startClient(builder -> {});
-    }
-
-    public HttpHost targetHost() {
-        return testResources.targetHost();
+        super(scheme, ClientProtocolLevel.STANDARD);
     }
 
     /**
@@ -112,24 +83,24 @@ public abstract class TestContentCodings {
      */
     @Test
     public void testResponseWithNoContent() throws Exception {
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", new HttpRequestHandler() {
-
-            /**
-             * {@inheritDoc}
-             */
-            @Override
-            public void handle(
-                    final ClassicHttpRequest request,
-                    final ClassicHttpResponse response,
-                    final HttpContext context) throws HttpException, IOException {
-                response.setCode(HttpStatus.SC_NO_CONTENT);
-            }
-        });
+        configureServer(bootstrap -> bootstrap
+                .register("*", new HttpRequestHandler() {
+
+                    /**
+                     * {@inheritDoc}
+                     */
+                    @Override
+                    public void handle(
+                            final ClassicHttpRequest request,
+                            final ClassicHttpResponse response,
+                            final HttpContext context) throws HttpException, IOException {
+                        response.setCode(HttpStatus.SC_NO_CONTENT);
+                    }
+                }));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         client.execute(target, request, response -> {
@@ -149,12 +120,12 @@ public abstract class TestContentCodings {
     public void testDeflateSupportForServerReturningRfc1950Stream() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, false));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createDeflateEncodingRequestHandler(entityText, false)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         client.execute(target, request, response -> {
@@ -174,12 +145,12 @@ public abstract class TestContentCodings {
     public void testDeflateSupportForServerReturningRfc1951Stream() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, true));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createDeflateEncodingRequestHandler(entityText, true)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         client.execute(target, request, response -> {
@@ -198,12 +169,12 @@ public abstract class TestContentCodings {
     public void testGzipSupport() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createGzipEncodingRequestHandler(entityText)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         client.execute(target, request, response -> {
@@ -223,13 +194,13 @@ public abstract class TestContentCodings {
     public void testThreadSafetyOfContentCodings() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createGzipEncodingRequestHandler(entityText)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final TestClient client = client();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
 
         /*
          * Create a load of workers which will access the resource. Half will use the default
@@ -273,12 +244,12 @@ public abstract class TestContentCodings {
     public void testHttpEntityWriteToForGzip() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createGzipEncodingRequestHandler(entityText)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         client.execute(target, request, response -> {
@@ -294,12 +265,12 @@ public abstract class TestContentCodings {
     public void testHttpEntityWriteToForDeflate() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, true));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createDeflateEncodingRequestHandler(entityText, true)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         client.execute(target, request, response -> {
@@ -314,12 +285,12 @@ public abstract class TestContentCodings {
     public void gzipResponsesWorkWithBasicResponseHandler() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createGzipEncodingRequestHandler(entityText)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         final String response = client.execute(target, request, new BasicHttpClientResponseHandler());
@@ -330,12 +301,12 @@ public abstract class TestContentCodings {
     public void deflateResponsesWorkWithBasicResponseHandler() throws Exception {
         final String entityText = "Hello, this is some plain text coming back.";
 
-        final ClassicTestServer server = startServer();
-        server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, false));
+        configureServer(bootstrap -> bootstrap
+                .register("*", createDeflateEncodingRequestHandler(entityText, false)));
 
-        final HttpHost target = targetHost();
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
 
         final HttpGet request = new HttpGet("/some-resource");
         final String response = client.execute(target, request, new BasicHttpClientResponseHandler());
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestCookieVirtualHost.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestCookieVirtualHost.java
index 195c89ebb..ff63fe28d 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestCookieVirtualHost.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestCookieVirtualHost.java
@@ -33,75 +33,71 @@ import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.cookie.BasicCookieStore;
 import org.apache.hc.client5.http.cookie.Cookie;
 import org.apache.hc.client5.http.cookie.CookieStore;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 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.message.BasicHeader;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * This class tests cookie matching when using Virtual Host.
  */
-public class TestCookieVirtualHost {
+public class TestCookieVirtualHost extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
+    public TestCookieVirtualHost() {
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+    }
 
     @Test
     public void testCookieMatchingWithVirtualHosts() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandlerVirtual("app.mydomain.fr", "*", (request, response, context) -> {
-
-            final int n = Integer.parseInt(request.getFirstHeader("X-Request").getValue());
-            switch (n) {
-            case 1:
-                // Assert Host is forwarded from URI
-                Assertions.assertEquals("app.mydomain.fr", request
-                        .getFirstHeader("Host").getValue());
-
-                response.setCode(HttpStatus.SC_OK);
-                // Respond with Set-Cookie on virtual host domain. This
-                // should be valid.
-                response.addHeader(new BasicHeader("Set-Cookie",
-                        "name1=value1; domain=mydomain.fr; path=/"));
-                break;
-
-            case 2:
-                // Assert Host is still forwarded from URI
-                Assertions.assertEquals("app.mydomain.fr", request
-                        .getFirstHeader("Host").getValue());
-
-                // We should get our cookie back.
-                Assertions.assertNotNull(request.getFirstHeader("Cookie"), "We must get a cookie header");
-                response.setCode(HttpStatus.SC_OK);
-                break;
-
-            case 3:
-                // Assert Host is forwarded from URI
-                Assertions.assertEquals("app.mydomain.fr", request
-                        .getFirstHeader("Host").getValue());
-
-                response.setCode(HttpStatus.SC_OK);
-                break;
-            default:
-                Assertions.fail("Unexpected value: " + n);
-                break;
-            }
-        });
-
-        final HttpHost target = testResources.targetHost();
-
-        final CloseableHttpClient client = testResources.startClient(b -> {});
+        configureServer(bootstrap -> bootstrap
+                .register("app.mydomain.fr", "*", (request, response, context) -> {
+
+                    final int n = Integer.parseInt(request.getFirstHeader("X-Request").getValue());
+                    switch (n) {
+                        case 1:
+                            // Assert Host is forwarded from URI
+                            Assertions.assertEquals("app.mydomain.fr", request
+                                    .getFirstHeader("Host").getValue());
+
+                            response.setCode(HttpStatus.SC_OK);
+                            // Respond with Set-Cookie on virtual host domain. This
+                            // should be valid.
+                            response.addHeader(new BasicHeader("Set-Cookie",
+                                    "name1=value1; domain=mydomain.fr; path=/"));
+                            break;
+
+                        case 2:
+                            // Assert Host is still forwarded from URI
+                            Assertions.assertEquals("app.mydomain.fr", request
+                                    .getFirstHeader("Host").getValue());
+
+                            // We should get our cookie back.
+                            Assertions.assertNotNull(request.getFirstHeader("Cookie"), "We must get a cookie header");
+                            response.setCode(HttpStatus.SC_OK);
+                            break;
+
+                        case 3:
+                            // Assert Host is forwarded from URI
+                            Assertions.assertEquals("app.mydomain.fr", request
+                                    .getFirstHeader("Host").getValue());
+
+                            response.setCode(HttpStatus.SC_OK);
+                            break;
+                        default:
+                            Assertions.fail("Unexpected value: " + n);
+                            break;
+                    }
+                }));
+
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
 
         final CookieStore cookieStore = new BasicCookieStore();
         final HttpClientContext context = HttpClientContext.create();
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestIdleConnectionEviction.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestIdleConnectionEviction.java
index dc2d26435..1b4e93205 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestIdleConnectionEviction.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestIdleConnectionEviction.java
@@ -35,32 +35,29 @@ import org.apache.hc.client5.http.impl.IdleConnectionEvictor;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
 import org.apache.hc.client5.testing.classic.RandomHandler;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
 import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
-public class TestIdleConnectionEviction {
+public class TestIdleConnectionEviction extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
+    public TestIdleConnectionEviction() {
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+    }
 
     @Test
     public void testIdleConnectionEviction() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = testResources.startClient(b -> {});
+        final TestClient client = client();
 
-        final PoolingHttpClientConnectionManager connManager = testResources.connManager();
+        final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
 
         connManager.setDefaultMaxPerRoute(10);
         connManager.setMaxTotal(50);
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMinimalClientRequestExecution.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMinimalClientRequestExecution.java
index 1681a4bdc..77c433285 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMinimalClientRequestExecution.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMinimalClientRequestExecution.java
@@ -32,9 +32,9 @@ import java.util.Locale;
 import java.util.Set;
 
 import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.impl.classic.MinimalHttpClient;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.Header;
@@ -47,29 +47,18 @@ import org.apache.hc.core5.http.io.HttpRequestHandler;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * Client protocol handling tests.
  */
-public abstract class TestMinimalClientRequestExecution {
-
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources;
+public abstract class TestMinimalClientRequestExecution extends AbstractIntegrationTestBase {
 
     protected TestMinimalClientRequestExecution(final URIScheme scheme) {
-        this.testResources = new TestClientResources(scheme, TIMEOUT);
+        super(scheme, ClientProtocolLevel.MINIMAL);
     }
 
-    public URIScheme scheme() {
-        return testResources.scheme();
-    }
     private static class SimpleService implements HttpRequestHandler {
 
         public SimpleService() {
@@ -89,11 +78,10 @@ public abstract class TestMinimalClientRequestExecution {
 
     @Test
     public void testNonCompliantURIWithContext() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
-        final MinimalHttpClient client = testResources.startMinimalClient();
+        final TestClient client = client();
 
         final HttpClientContext context = HttpClientContext.create();
         for (int i = 0; i < 10; i++) {
@@ -121,11 +109,10 @@ public abstract class TestMinimalClientRequestExecution {
 
     @Test
     public void testNonCompliantURIWithoutContext() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
-        final MinimalHttpClient client = testResources.startMinimalClient();
+        final TestClient client = client();
 
         for (int i = 0; i < 10; i++) {
             final HttpGet request = new HttpGet("/");
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
index a881d1750..f576cf891 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
@@ -33,7 +33,6 @@ import java.net.URI;
 import java.util.Collections;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.function.Consumer;
 
 import org.apache.hc.client5.http.CircularRedirectException;
 import org.apache.hc.client5.http.ClientProtocolException;
@@ -43,8 +42,6 @@ import org.apache.hc.client5.http.classic.methods.HttpPost;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.cookie.BasicCookieStore;
 import org.apache.hc.client5.http.cookie.CookieStore;
-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.cookie.BasicClientCookie;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.protocol.RedirectLocations;
@@ -53,7 +50,8 @@ import org.apache.hc.client5.testing.classic.EchoHandler;
 import org.apache.hc.client5.testing.classic.RandomHandler;
 import org.apache.hc.client5.testing.classic.RedirectingDecorator;
 import org.apache.hc.client5.testing.redirect.Redirect;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.Header;
@@ -69,59 +67,31 @@ import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.http.protocol.HttpProcessor;
 import org.apache.hc.core5.net.URIBuilder;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
-import org.apache.hc.core5.util.Timeout;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * Redirection test cases.
  */
-public abstract class TestRedirects {
-
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
-
-    @RegisterExtension
-    private TestClientResources testResources;
+public abstract class TestRedirects extends AbstractIntegrationTestBase {
 
     protected TestRedirects(final URIScheme scheme) {
-        this.testResources = new TestClientResources(scheme, TIMEOUT);
-    }
-
-    public URIScheme scheme() {
-        return testResources.scheme();
-    }
-
-    public ClassicTestServer startServer(final HttpProcessor httpProcessor,
-                                         final Decorator<HttpServerRequestHandler> handlerDecorator) throws IOException {
-        return testResources.startServer(null, httpProcessor, handlerDecorator);
-    }
-
-    public CloseableHttpClient startClient(final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
-        return testResources.startClient(clientCustomizer);
-    }
-
-    public CloseableHttpClient startClient() throws Exception {
-        return testResources.startClient(builder -> {});
-    }
-
-    public HttpHost targetHost() {
-        return testResources.targetHost();
+        super(scheme, ClientProtocolLevel.STANDARD);
     }
 
     @Test
     public void testBasicRedirect300() throws Exception {
-        final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES)))
+                .register("/random/*", new RandomHandler())
+        );
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/100");
@@ -141,14 +111,16 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect300NoKeepAlive() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES,
-                        Redirect.ConnControl.CLOSE)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MULTIPLE_CHOICES,
+                                Redirect.ConnControl.CLOSE)))
+                .register("/random/*", new RandomHandler())
+        );
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/100");
@@ -169,13 +141,14 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect301() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_PERMANENTLY)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/100");
@@ -200,13 +173,14 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect302() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/50");
@@ -225,19 +199,20 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect302NoLocation() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.startsWith("/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, null);
-                    }
-                    return null;
-                }));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.startsWith("/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, null);
+                            }
+                            return null;
+                        }))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/100");
@@ -255,13 +230,14 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect303() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_SEE_OTHER)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_SEE_OTHER)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/123");
@@ -279,16 +255,15 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect304() throws Exception {
-        final ClassicTestServer server = startServer(null ,null);
-        server.registerHandler("/oldlocation/*", (request, response, context) -> {
-            response.setCode(HttpStatus.SC_NOT_MODIFIED);
-            response.addHeader(HttpHeaders.LOCATION, "/random/100");
-        });
-
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/oldlocation/*", (request, response, context) -> {
+                    response.setCode(HttpStatus.SC_NOT_MODIFIED);
+                    response.addHeader(HttpHeaders.LOCATION, "/random/100");
+                })
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/stuff");
@@ -310,16 +285,15 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect305() throws Exception {
-        final ClassicTestServer server =  startServer(null ,null);
-        server.registerHandler("/oldlocation/*", (request, response, context) -> {
-            response.setCode(HttpStatus.SC_USE_PROXY);
-            response.addHeader(HttpHeaders.LOCATION, "/random/100");
-        });
-
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .register("/oldlocation/*", (request, response, context) -> {
+                    response.setCode(HttpStatus.SC_USE_PROXY);
+                    response.addHeader(HttpHeaders.LOCATION, "/random/100");
+                })
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/stuff");
@@ -341,13 +315,14 @@ public abstract class TestRedirects {
 
     @Test
     public void testBasicRedirect307() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_TEMPORARY_REDIRECT)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_TEMPORARY_REDIRECT)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/123");
@@ -365,14 +340,15 @@ public abstract class TestRedirects {
 
     @Test
     public void testMaxRedirectCheck() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
-                        HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
+                                HttpStatus.SC_MOVED_TEMPORARILY)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final RequestConfig config = RequestConfig.custom()
                 .setCircularRedirectsAllowed(true)
                 .setMaxRedirects(5)
@@ -387,14 +363,16 @@ public abstract class TestRedirects {
 
     @Test
     public void testCircularRedirect() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
-                        HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/circular-oldlocation/", "/circular-oldlocation/",
+                                HttpStatus.SC_MOVED_TEMPORARILY)))
+                .register("/random/*", new RandomHandler()));
+
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final RequestConfig config = RequestConfig.custom()
                 .setCircularRedirectsAllowed(false)
                 .build();
@@ -408,13 +386,14 @@ public abstract class TestRedirects {
 
     @Test
     public void testPostRedirectSeeOther() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/echo", HttpStatus.SC_SEE_OTHER)));
-        server.registerHandler("/echo/*", new EchoHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/echo", HttpStatus.SC_SEE_OTHER)))
+                .register("/echo/*", new EchoHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpPost httppost = new HttpPost("/oldlocation/stuff");
@@ -434,19 +413,21 @@ public abstract class TestRedirects {
 
     @Test
     public void testRelativeRedirect() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.startsWith("/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/random/100");
-
-                    }
-                    return null;
-                }));        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.startsWith("/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/random/100");
+
+                            }
+                            return null;
+                        }))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/stuff");
@@ -464,20 +445,21 @@ public abstract class TestRedirects {
 
     @Test
     public void testRelativeRedirect2() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.equals("/random/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "100");
-
-                    }
-                    return null;
-                }));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.equals("/random/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "100");
+
+                            }
+                            return null;
+                        }))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/random/oldlocation");
@@ -495,20 +477,21 @@ public abstract class TestRedirects {
 
     @Test
     public void testRejectBogusRedirectLocation() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.equals("/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "xxx://bogus");
-
-                    }
-                    return null;
-                }));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.equals("/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "xxx://bogus");
+
+                            }
+                            return null;
+                        }))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpGet httpget = new HttpGet("/oldlocation");
 
         final ClientProtocolException exception = Assertions.assertThrows(ClientProtocolException.class, () ->
@@ -518,20 +501,21 @@ public abstract class TestRedirects {
 
     @Test
     public void testRejectInvalidRedirectLocation() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                requestUri -> {
-                    final String path = requestUri.getPath();
-                    if (path.equals("/oldlocation")) {
-                        return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/newlocation/?p=I have spaces");
-
-                    }
-                    return null;
-                }));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
-
-        final CloseableHttpClient client = startClient();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        requestUri -> {
+                            final String path = requestUri.getPath();
+                            if (path.equals("/oldlocation")) {
+                                return new Redirect(HttpStatus.SC_MOVED_TEMPORARILY, "/newlocation/?p=I have spaces");
+
+                            }
+                            return null;
+                        }))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
+
+        final TestClient client = client();
         final HttpGet httpget = new HttpGet("/oldlocation");
 
         final ClientProtocolException exception = Assertions.assertThrows(ClientProtocolException.class, () ->
@@ -541,13 +525,14 @@ public abstract class TestRedirects {
 
     @Test
     public void testRedirectWithCookie() throws Exception {
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final CookieStore cookieStore = new BasicCookieStore();
 
         final BasicClientCookie cookie = new BasicClientCookie("name", "value");
@@ -577,16 +562,18 @@ public abstract class TestRedirects {
 
     @Test
     public void testDefaultHeadersRedirect() throws Exception {
-        final CloseableHttpClient client = startClient(builder -> builder
+        configureClient(builder -> builder
                 .setDefaultHeaders(Collections.singletonList(new BasicHeader(HttpHeaders.USER_AGENT, "my-test-client")))
         );
 
-         final ClassicTestServer server = startServer(null, requestHandler -> new RedirectingDecorator(
-                requestHandler,
-                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)));
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(requestHandler -> new RedirectingDecorator(
+                        requestHandler,
+                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)))
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/100");
@@ -608,33 +595,34 @@ public abstract class TestRedirects {
     @Test
     public void testCompressionHeaderRedirect() throws Exception {
         final Queue<String> values = new ConcurrentLinkedQueue<>();
-         final ClassicTestServer server = startServer(null, new Decorator<HttpServerRequestHandler>() {
-
-            @Override
-            public HttpServerRequestHandler decorate(final HttpServerRequestHandler requestHandler) {
-                return new RedirectingDecorator(
-                        requestHandler,
-                        new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)) {
+        configureServer(bootstrap -> bootstrap
+                .setExchangeHandlerDecorator(new Decorator<HttpServerRequestHandler>() {
 
                     @Override
-                    public void handle(final ClassicHttpRequest request,
-                                       final ResponseTrigger responseTrigger,
-                                       final HttpContext context) throws HttpException, IOException {
-                        final Header header = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
-                        if (header != null) {
-                            values.add(header.getValue());
-                        }
-                        super.handle(request, responseTrigger, context);
+                    public HttpServerRequestHandler decorate(final HttpServerRequestHandler requestHandler) {
+                        return new RedirectingDecorator(
+                                requestHandler,
+                                new OldPathRedirectResolver("/oldlocation", "/random", HttpStatus.SC_MOVED_TEMPORARILY)) {
+
+                            @Override
+                            public void handle(final ClassicHttpRequest request,
+                                               final ResponseTrigger responseTrigger,
+                                               final HttpContext context) throws HttpException, IOException {
+                                final Header header = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
+                                if (header != null) {
+                                    values.add(header.getValue());
+                                }
+                                super.handle(request, responseTrigger, context);
+                            }
+
+                        };
                     }
 
-                };
-            }
-
-        });
-        server.registerHandler("/random/*", new RandomHandler());
-        final HttpHost target = targetHost();
+                })
+                .register("/random/*", new RandomHandler()));
+        final HttpHost target = startServer();
 
-        final CloseableHttpClient client = startClient();
+        final TestClient client = client();
         final HttpClientContext context = HttpClientContext.create();
 
         final HttpGet httpget = new HttpGet("/oldlocation/100");
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestStatefulConnManagement.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestStatefulConnManagement.java
index d31f50dce..d4451d5e5 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestStatefulConnManagement.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestStatefulConnManagement.java
@@ -31,8 +31,10 @@ import java.io.IOException;
 import org.apache.hc.client5.http.UserTokenHandler;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.testing.sync.extension.TestClientResources;
+import org.apache.hc.client5.testing.sync.extension.ClientProtocolLevel;
+import org.apache.hc.client5.testing.sync.extension.TestClient;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.EndpointDetails;
@@ -44,22 +46,20 @@ import org.apache.hc.core5.http.io.HttpRequestHandler;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
 import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 /**
  * Test cases for state-ful connections.
  */
-public class TestStatefulConnManagement {
+public class TestStatefulConnManagement extends AbstractIntegrationTestBase {
 
-    public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
     public static final Timeout LONG_TIMEOUT = Timeout.ofMinutes(3);
 
-    @RegisterExtension
-    private TestClientResources testResources = new TestClientResources(URIScheme.HTTP, TIMEOUT);
+    public TestStatefulConnManagement() {
+        super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+    }
 
     private static class SimpleService implements HttpRequestHandler {
 
@@ -80,9 +80,9 @@ public class TestStatefulConnManagement {
 
     @Test
     public void testStatefulConnections() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap
+                .register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
         final int workerCount = 5;
         final int requestCount = 5;
@@ -92,13 +92,13 @@ public class TestStatefulConnManagement {
             return id;
         };
 
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(workerCount)
-                        .setMaxConnPerRoute(workerCount),
-                builder -> builder
-                        .setUserTokenHandler(userTokenHandler)
-        );
+        configureClient(builder -> builder
+                .setUserTokenHandler(userTokenHandler));
+        final TestClient client = client();
+
+        final PoolingHttpClientConnectionManager connectionManager = client.getConnectionManager();
+        connectionManager.setMaxTotal(workerCount);
+        connectionManager.setDefaultMaxPerRoute(workerCount);
 
         final HttpClientContext[] contexts = new HttpClientContext[workerCount];
         final HttpWorker[] workers = new HttpWorker[workerCount];
@@ -194,9 +194,8 @@ public class TestStatefulConnManagement {
 
     @Test
     public void testRouteSpecificPoolRecylcing() throws Exception {
-        final ClassicTestServer server = testResources.startServer(null, null, null);
-        server.registerHandler("*", new SimpleService());
-        final HttpHost target = testResources.targetHost();
+        configureServer(bootstrap -> bootstrap.register("*", new SimpleService()));
+        final HttpHost target = startServer();
 
         // This tests what happens when a maxed connection pool needs
         // to kill the last idle connection to a route to build a new
@@ -204,16 +203,13 @@ public class TestStatefulConnManagement {
 
         final int maxConn = 2;
 
+        configureClient(builder -> builder
+                .setUserTokenHandler((route, context) -> context.getAttribute("user")));
+        final TestClient client = client();
 
-        final UserTokenHandler userTokenHandler = (route, context) -> context.getAttribute("user");
-
-        final CloseableHttpClient client = testResources.startClient(
-                builder -> builder
-                        .setMaxConnTotal(maxConn)
-                        .setMaxConnPerRoute(maxConn),
-                builder -> builder
-                        .setUserTokenHandler(userTokenHandler)
-        );
+        final PoolingHttpClientConnectionManager connectionManager = client.getConnectionManager();
+        connectionManager.setMaxTotal(maxConn);
+        connectionManager.setDefaultMaxPerRoute(maxConn);
 
         // Bottom of the pool : a *keep alive* connection to Route 1.
         final HttpContext context1 = HttpClientContext.create();
@@ -232,7 +228,7 @@ public class TestStatefulConnManagement {
         // Send a very simple HTTP get (it MUST be simple, no auth, no proxy, no 302, no 401, ...)
         // Send it to another route. Must be a keepalive.
         final HttpContext context2 = HttpClientContext.create();
-        client.execute(new HttpHost("127.0.0.1", server.getPort()), new HttpGet("/"), context2, response -> {
+        client.execute(new HttpHost("127.0.0.1", target.getPort()), new HttpGet("/"), context2, response -> {
             EntityUtils.consume(response.getEntity());
             return null;
         });
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/ClientProtocolLevel.java
similarity index 62%
copy from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
copy to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/ClientProtocolLevel.java
index e6485229c..53000d5cd 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2Async.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/ClientProtocolLevel.java
@@ -24,27 +24,11 @@
  * <http://www.apache.org/>.
  *
  */
-package org.apache.hc.client5.testing.async;
 
-import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http2.config.H2Config;
-import org.apache.hc.core5.testing.nio.H2TestServer;
+package org.apache.hc.client5.testing.sync.extension;
 
-public abstract class TestH2Async extends AbstractHttpAsyncFundamentalsTest<CloseableHttpAsyncClient> {
+public enum ClientProtocolLevel {
 
-    public TestH2Async(final URIScheme scheme) {
-        super(scheme);
-    }
+    STANDARD, MINIMAL
 
-    @Override
-    protected H2TestServer startServer() throws Exception {
-        return startServer(H2Config.DEFAULT, null, null);
-    }
-
-    @Override
-    protected CloseableHttpAsyncClient startClient() throws Exception {
-        return startH2Client(b -> {});
-    }
-
-}
\ No newline at end of file
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/MinimalTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/MinimalTestClientBuilder.java
new file mode 100644
index 000000000..bc7686eb9
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/MinimalTestClientBuilder.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.sync.extension;
+
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.classic.MinimalHttpClient;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.util.Timeout;
+
+final class MinimalTestClientBuilder implements TestClientBuilder {
+
+    private Timeout timeout;
+
+    private HttpClientConnectionManager connectionManager;
+
+    public MinimalTestClientBuilder() {
+    }
+
+    @Override
+    public ClientProtocolLevel getProtocolLevel() {
+        return ClientProtocolLevel.MINIMAL;
+    }
+
+    @Override
+    public TestClientBuilder setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setConnectionManager(final HttpClientConnectionManager connectionManager) {
+        this.connectionManager = connectionManager;
+        return this;
+    }
+
+    @Override
+    public TestClient build() throws Exception {
+        final HttpClientConnectionManager connectionManagerCopy = connectionManager != null ? connectionManager :
+                PoolingHttpClientConnectionManagerBuilder.create()
+                        .setTlsSocketStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                        .setDefaultSocketConfig(SocketConfig.custom()
+                                .setSoTimeout(timeout)
+                                .build())
+                        .setDefaultConnectionConfig(ConnectionConfig.custom()
+                                .setConnectTimeout(timeout)
+                                .build())
+                        .build();
+
+        final MinimalHttpClient minimalClient = HttpClients.createMinimal(connectionManagerCopy);
+        return new TestClient(minimalClient, connectionManagerCopy);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/StandardTestClientBuilder.java
new file mode 100644
index 000000000..378737858
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/StandardTestClientBuilder.java
@@ -0,0 +1,159 @@
+/*
+ * ====================================================================
+ * 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.sync.extension;
+
+import java.util.Collection;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.UserTokenHandler;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+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.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.util.Timeout;
+
+final class StandardTestClientBuilder implements TestClientBuilder {
+
+    private final HttpClientBuilder clientBuilder;
+
+    private Timeout timeout;
+
+    private HttpClientConnectionManager connectionManager;
+
+    public StandardTestClientBuilder() {
+        this.clientBuilder = HttpClientBuilder.create();
+    }
+
+    @Override
+    public ClientProtocolLevel getProtocolLevel() {
+        return ClientProtocolLevel.STANDARD;
+    }
+
+    @Override
+    public TestClientBuilder setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setConnectionManager(final HttpClientConnectionManager connectionManager) {
+        this.connectionManager = connectionManager;
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
+        this.clientBuilder.addResponseInterceptorFirst(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder addResponseInterceptorLast(final HttpResponseInterceptor interceptor) {
+        this.clientBuilder.addResponseInterceptorLast(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder addRequestInterceptorFirst(final HttpRequestInterceptor interceptor) {
+        this.clientBuilder.addRequestInterceptorFirst(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder addRequestInterceptorLast(final HttpRequestInterceptor interceptor) {
+        this.clientBuilder.addRequestInterceptorLast(interceptor);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setUserTokenHandler(final UserTokenHandler userTokenHandler) {
+        this.clientBuilder.setUserTokenHandler(userTokenHandler);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
+        this.clientBuilder.setDefaultHeaders(defaultHeaders);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
+        this.clientBuilder.setRetryStrategy(retryStrategy);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStrategy) {
+        this.clientBuilder.setTargetAuthenticationStrategy(targetAuthStrategy);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setDefaultAuthSchemeRegistry(final Lookup<AuthSchemeFactory> authSchemeRegistry) {
+        this.clientBuilder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+        return this;
+    }
+
+    @Override
+    public TestClientBuilder setRequestExecutor(final HttpRequestExecutor requestExec) {
+        this.clientBuilder.setRequestExecutor(requestExec);
+        return this;
+    }
+
+    @Override
+    public TestClient build() throws Exception {
+        final HttpClientConnectionManager connectionManagerCopy = connectionManager != null ? connectionManager :
+                PoolingHttpClientConnectionManagerBuilder.create()
+                        .setTlsSocketStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
+                        .setDefaultSocketConfig(SocketConfig.custom()
+                                .setSoTimeout(timeout)
+                                .build())
+                        .setDefaultConnectionConfig(ConnectionConfig.custom()
+                                .setConnectTimeout(timeout)
+                                .build())
+                        .build();
+
+        final CloseableHttpClient client = clientBuilder
+                .setConnectionManager(connectionManagerCopy)
+                .build();
+        return new TestClient(client, connectionManagerCopy);
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClient.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClient.java
new file mode 100644
index 000000000..e17e845c7
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClient.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * 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.sync.extension;
+
+import java.io.IOException;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.Args;
+
+public class TestClient extends CloseableHttpClient {
+
+    private final CloseableHttpClient client;
+    private final HttpClientConnectionManager connectionManager;
+
+    public TestClient(final CloseableHttpClient client,
+                      final HttpClientConnectionManager connectionManager) {
+        this.client = Args.notNull(client, "Client");
+        this.connectionManager = Args.notNull(connectionManager, "Connection manager");
+    }
+
+    @Override
+    public void close(final CloseMode closeMode) {
+        client.close(closeMode);
+    }
+
+    @Override
+    public void close() throws IOException {
+        client.close();
+    }
+
+    @Override
+    protected CloseableHttpResponse doExecute(final HttpHost target, final ClassicHttpRequest request, final HttpContext context) throws IOException {
+        return CloseableHttpResponse.adapt(client.executeOpen(target, request, context));
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends CloseableHttpClient> T getImplementation() {
+        return (T) client;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends HttpClientConnectionManager> T getConnectionManager() {
+        return (T) connectionManager;
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientBuilder.java
new file mode 100644
index 000000000..27a0381eb
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientBuilder.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * 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.sync.extension;
+
+import java.util.Collection;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.UserTokenHandler;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
+import org.apache.hc.core5.util.Timeout;
+
+public interface TestClientBuilder {
+
+    ClientProtocolLevel getProtocolLevel();
+
+    TestClientBuilder setTimeout(Timeout soTimeout);
+
+    TestClientBuilder setConnectionManager(HttpClientConnectionManager connManager);
+
+    default TestClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
+        return this;
+    }
+
+    default TestClientBuilder addResponseInterceptorLast(HttpResponseInterceptor interceptor) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder addRequestInterceptorFirst(HttpRequestInterceptor interceptor) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder addRequestInterceptorLast(HttpRequestInterceptor interceptor) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder setUserTokenHandler(UserTokenHandler userTokenHandler) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder setDefaultHeaders(Collection<? extends Header> defaultHeaders) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder setRetryStrategy(HttpRequestRetryStrategy retryStrategy) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder setDefaultAuthSchemeRegistry(Lookup<AuthSchemeFactory> authSchemeRegistry) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    default TestClientBuilder setRequestExecutor(HttpRequestExecutor requestExec) {
+        throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+    }
+
+    TestClient build() throws Exception;
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientResources.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientResources.java
index fd8f0000a..64758f58b 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientResources.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestClientResources.java
@@ -27,59 +27,46 @@
 
 package org.apache.hc.client5.testing.sync.extension;
 
-import java.io.IOException;
 import java.util.function.Consumer;
 
-import org.apache.hc.client5.http.config.ConnectionConfig;
-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.classic.MinimalHttpClient;
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
-import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
-import org.apache.hc.client5.testing.SSLTestContexts;
-import org.apache.hc.core5.function.Decorator;
-import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.config.Http1Config;
-import org.apache.hc.core5.http.io.HttpServerRequestHandler;
-import org.apache.hc.core5.http.io.SocketConfig;
-import org.apache.hc.core5.http.protocol.HttpProcessor;
 import org.apache.hc.core5.io.CloseMode;
-import org.apache.hc.core5.testing.classic.ClassicTestServer;
+import org.apache.hc.core5.util.Asserts;
 import org.apache.hc.core5.util.Timeout;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.extension.AfterEachCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class TestClientResources implements BeforeEachCallback, AfterEachCallback {
+public class TestClientResources implements AfterEachCallback {
 
     private static final Logger LOG = LoggerFactory.getLogger(TestClientResources.class);
 
     private final URIScheme scheme;
     private final Timeout timeout;
+    private final ClientProtocolLevel clientProtocolLevel;
+    private final TestServerBootstrap serverBootstrap;
+    private final TestClientBuilder clientBuilder;
 
-    private ClassicTestServer server;
+    private TestServer server;
     private PoolingHttpClientConnectionManager connManager;
-    private CloseableHttpClient client;
+    private TestClient client;
 
-    public TestClientResources(final URIScheme scheme, final Timeout timeout) {
-        this.scheme = scheme;
+    public TestClientResources(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final Timeout timeout) {
+        this.scheme = scheme != null ? scheme : URIScheme.HTTP;
         this.timeout = timeout;
-    }
-
-    @Override
-    public void beforeEach(final ExtensionContext extensionContext) throws Exception {
-        LOG.debug("Starting up test server");
-        server = new ClassicTestServer(
-                scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null,
-                SocketConfig.custom()
-                        .setSoTimeout(timeout)
-                        .build());
+        this.clientProtocolLevel = clientProtocolLevel != null ? clientProtocolLevel : ClientProtocolLevel.STANDARD;
+        this.serverBootstrap = new TestServerBootstrap(this.scheme)
+                .setTimeout(this.timeout);
+        switch (this.clientProtocolLevel) {
+            case MINIMAL:
+                this.clientBuilder = new MinimalTestClientBuilder();
+                break;
+            default:
+                this.clientBuilder = new StandardTestClientBuilder();
+        }
+        this.clientBuilder.setTimeout(timeout);
     }
 
     @Override
@@ -103,72 +90,32 @@ public class TestClientResources implements BeforeEachCallback, AfterEachCallbac
         return this.scheme;
     }
 
-    public ClassicTestServer startServer(
-            final Http1Config http1Config,
-            final HttpProcessor httpProcessor,
-            final Decorator<HttpServerRequestHandler> handlerDecorator) throws IOException {
-        Assertions.assertNotNull(server);
-        server.start(http1Config, httpProcessor, handlerDecorator);
-        return server;
-    }
-
-    public HttpHost targetHost() {
-        Assertions.assertNotNull(server);
-        return new HttpHost(scheme.id, "localhost", server.getPort());
+    public ClientProtocolLevel getClientProtocolLevel() {
+        return clientProtocolLevel;
     }
 
-    public CloseableHttpClient startClient(
-            final Consumer<PoolingHttpClientConnectionManagerBuilder> connManagerCustomizer,
-            final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
-        Assertions.assertNull(connManager);
-        Assertions.assertNull(client);
-
-        final PoolingHttpClientConnectionManagerBuilder connManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
-        connManagerBuilder.setTlsSocketStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
-        connManagerBuilder.setDefaultSocketConfig(SocketConfig.custom()
-                .setSoTimeout(timeout)
-                .build());
-        connManagerBuilder.setDefaultConnectionConfig(ConnectionConfig.custom()
-                .setConnectTimeout(timeout)
-                .build());
-        connManagerCustomizer.accept(connManagerBuilder);
-
-        connManager = connManagerBuilder.build();
-
-        final HttpClientBuilder clientBuilder = HttpClientBuilder.create()
-                .setConnectionManager(connManager);
-        clientCustomizer.accept(clientBuilder);
-        client = clientBuilder.build();
-        return client;
+    public void configureServer(final Consumer<TestServerBootstrap> serverCustomizer) {
+        Asserts.check(server == null, "Server is already running and cannot be changed");
+        serverCustomizer.accept(serverBootstrap);
     }
 
-    public MinimalHttpClient startMinimalClient() throws Exception {
-        Assertions.assertNull(connManager);
-        Assertions.assertNull(client);
-
-        final PoolingHttpClientConnectionManagerBuilder connManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
-        connManagerBuilder.setTlsSocketStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()));
-        connManagerBuilder.setDefaultSocketConfig(SocketConfig.custom()
-                .setSoTimeout(timeout)
-                .build());
-        connManagerBuilder.setDefaultConnectionConfig(ConnectionConfig.custom()
-                .setConnectTimeout(timeout)
-                .build());
-        connManager = connManagerBuilder.build();
-
-        final MinimalHttpClient minimalClient = HttpClients.createMinimal(connManager);
-        client = minimalClient;
-        return minimalClient;
+    public TestServer server() throws Exception {
+        if (server == null) {
+            server = serverBootstrap.build();
+        }
+        return server;
     }
 
-    public CloseableHttpClient startClient(
-            final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
-        return startClient(b -> {}, clientCustomizer);
+    public void configureClient(final Consumer<TestClientBuilder> clientCustomizer) {
+        Asserts.check(client == null, "Client is already running and cannot be changed");
+        clientCustomizer.accept(clientBuilder);
     }
 
-    public PoolingHttpClientConnectionManager connManager() {
-        Assertions.assertNotNull(connManager);
-        return connManager;
+    public TestClient client() throws Exception {
+        if (client == null) {
+            client = clientBuilder.build();
+        }
+        return client;
     }
 
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestServer.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestServer.java
new file mode 100644
index 000000000..7c0f69421
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestServer.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * 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.sync.extension;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.io.HttpServerRequestHandler;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.testing.classic.ClassicTestServer;
+
+public class TestServer {
+
+    private final ClassicTestServer server;
+    private final Http1Config http1Config;
+    private final HttpProcessor httpProcessor;
+    private final Decorator<HttpServerRequestHandler> exchangeHandlerDecorator;
+
+    TestServer(
+            final ClassicTestServer server,
+            final Http1Config http1Config,
+            final HttpProcessor httpProcessor,
+            final Decorator<HttpServerRequestHandler> exchangeHandlerDecorator) {
+        this.server = server;
+        this.http1Config = http1Config;
+        this.httpProcessor = httpProcessor;
+        this.exchangeHandlerDecorator = exchangeHandlerDecorator;
+    }
+
+    public void shutdown(final CloseMode closeMode) {
+        server.shutdown(closeMode);
+    }
+
+    public InetSocketAddress start() throws IOException {
+        server.start(http1Config, httpProcessor, exchangeHandlerDecorator);
+        return new InetSocketAddress(server.getInetAddress(), server.getPort());
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestServerBootstrap.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestServerBootstrap.java
new file mode 100644
index 000000000..51de842ab
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/extension/TestServerBootstrap.java
@@ -0,0 +1,123 @@
+/*
+ * ====================================================================
+ * 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.sync.extension;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.HttpServerRequestHandler;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.testing.classic.ClassicTestServer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+
+public class TestServerBootstrap {
+
+    static final class HandlerEntry<T> {
+
+        final String hostname;
+        final String uriPattern;
+        final T handler;
+
+        public HandlerEntry(final String hostname, final String uriPattern, final T handler) {
+            this.hostname = hostname;
+            this.uriPattern = uriPattern;
+            this.handler = handler;
+        }
+
+        public HandlerEntry(final String uriPattern, final T handler) {
+            this(null, uriPattern, handler);
+        }
+
+    }
+
+    private final URIScheme scheme;
+
+    private final List<HandlerEntry<HttpRequestHandler>> handlerList;
+    private Timeout timeout;
+    private HttpProcessor httpProcessor;
+    private Decorator<HttpServerRequestHandler> exchangeHandlerDecorator;
+
+    public TestServerBootstrap(final URIScheme scheme) {
+        this.scheme = scheme != null ? scheme : URIScheme.HTTP;
+        this.handlerList = new ArrayList<>();
+    }
+
+    public TestServerBootstrap register(final String uriPattern, final HttpRequestHandler requestHandler) {
+        return register(null, uriPattern, requestHandler);
+    }
+
+    public TestServerBootstrap register(final String hostname, final String uriPattern, final HttpRequestHandler requestHandler) {
+        Args.notNull(uriPattern, "URI pattern");
+        Args.notNull(requestHandler, "Exchange handler");
+        handlerList.add(new HandlerEntry<>(hostname, uriPattern, requestHandler));
+        return this;
+    }
+
+    public TestServerBootstrap setTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    public TestServerBootstrap setHttpProcessor(final HttpProcessor httpProcessor) {
+        this.httpProcessor = httpProcessor;
+        return this;
+    }
+
+    public TestServerBootstrap setExchangeHandlerDecorator(final Decorator<HttpServerRequestHandler> exchangeHandlerDecorator) {
+        this.exchangeHandlerDecorator = exchangeHandlerDecorator;
+        return this;
+    }
+
+    public TestServer build() throws Exception {
+        final ClassicTestServer server = new ClassicTestServer(
+                scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null,
+                SocketConfig.custom()
+                        .setSoTimeout(timeout)
+                        .build());
+        for (final HandlerEntry<HttpRequestHandler> entry: handlerList) {
+            if (entry.hostname != null) {
+                server.registerHandlerVirtual(entry.hostname, entry.uriPattern, entry.handler);
+            } else {
+                server.registerHandler(entry.uriPattern, entry.handler);
+            }
+        }
+        return new TestServer(
+                server,
+                Http1Config.DEFAULT,
+                httpProcessor,
+                exchangeHandlerDecorator);
+    }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/CloseableHttpResponse.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/CloseableHttpResponse.java
index a936d069f..e01b58f9b 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/CloseableHttpResponse.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/CloseableHttpResponse.java
@@ -32,6 +32,7 @@ import java.util.Iterator;
 import java.util.Locale;
 
 import org.apache.hc.client5.http.classic.ExecRuntime;
+import org.apache.hc.core5.annotation.Internal;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
@@ -49,7 +50,11 @@ public final class CloseableHttpResponse implements ClassicHttpResponse {
     private final ClassicHttpResponse response;
     private final ExecRuntime execRuntime;
 
-    static CloseableHttpResponse adapt(final ClassicHttpResponse response) {
+    /**
+     * @since 5.4
+     */
+    @Internal
+    public static CloseableHttpResponse adapt(final ClassicHttpResponse response) {
         if (response == null) {
             return null;
         }