Repository: EventStore/EventStoreDB-Client-Java
Pull request #236 (2023-08-15 20:11:38)

Add secure and cluster tests.
Changed: Add secure and cluster tests.

PR doesn't have as much changes as the diff suggests. Tests were re-organized so those can run regardless of the database topology:

* Single node
* Secure (still single node but with certificates configured)
* Cluster (3 nodes)

Tests were almost gathered based on the scope of the ESDB api minus one caveat (Expectations):

* Streams
* Persistent Subscriptions.

Expectations tests are based on the image produced by the [testdata] repo. Those tests run a prepopulated database so we can test invariant behaviours across all clients. Those expectations tests are mostly reads operations including (persistent) subscriptions. There are projection tests too. Expectations tests do not run on a cluster, just single node setup.

Finally, this PR adds a dispatch action so we can test a specific server version manually. The version should match a version we deployed on our main repo docker registry.

[testdata]: https://github.com/EventStore/EventStore-Client-gRPC-TestData

linear[bot]: <details>
<summary><a href="https://linear.app/eventstore/issue/DB-334/improve-java-client-testing">DB-334 Improve Java client testing</a></summary>
<p>

We do not have tests running on a cluster for example.
</p>
</details>

Patch:
diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml
index f0addc2..ebd9b68 100644
--- a/.github/workflows/pull-requests.yml
+++ b/.github/workflows/pull-requests.yml
@@ -6,61 +6,40 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        java: ["8", "11", "14"]
+        java: [8, 11, 17]
     runs-on: ubuntu-latest
 
     steps:
       - name: Check Out Sources
-        uses: actions/checkout@v1
+        uses: actions/checkout@v3
 
-      - name: Install JDK ${{ matrix.java }}
-        uses: actions/setup-java@v1
+      - name: Set up JDK ${{ matrix.java }}
+        uses: actions/setup-java@v3
         with:
           java-version: ${{ matrix.java }}
-      - name: Confirm Java Version
-        shell: bash
-        run: javac -version
+          distribution: 'temurin'
 
-      - name: Cache Gradle packages
-        uses: actions/cache@v2
+      - name: Setup Gradle
+        uses: gradle/gradle-build-action@v2
         with:
-          path: ~/.gradle/caches
-          key: ubuntu-latest-${{ matrix.java }}-gradle-${{ hashFiles('**/*.gradle') }}
-          restore-keys: ubuntu-latest-${{ matrix.java }}-gradle
+          gradle-version: 7.4
 
-      - name: Build with JDK ${{ matrix.java }}
-        uses: eskatos/gradle-command-action@v1
-        with:
-          arguments: compileJava
+      - name: Build
+        run: ./gradlew compileTest
+
+      # Tests that do not require a database connection.
+      - name: Misc tests
+        run: ./gradlew ci --tests MiscTests
 
-  test:
-    name: Run tests
+  tests:
     needs: build
+    name: Tests
+
     strategy:
       fail-fast: false
       matrix:
-         version: [ previous-lts, lts, ci ]
-
-    runs-on: ubuntu-latest
-    steps:
-     - name: Check Out Sources
-       uses: actions/checkout@v1
-
-     - name: Install JDK 8
-       uses: actions/setup-java@v1
-       with:
-         java-version: 8
-
-     - name: Login to GitHub Container Registry
-       uses: docker/login-action@v1
-       with:
-         registry: ghcr.io
-         username: ${{ github.actor }}
-         password: ${{ secrets.GITHUB_TOKEN }}
+        version: [previous-lts, lts, latest, ci]
 
-     - name: Test on EventStoreDB version ${{ matrix.version }}
-       uses: eskatos/gradle-command-action@v1
-       env:
-         EVENTSTORE_DOCKER_TAG_ENV: ${{ matrix.version }}
-       with:
-         arguments: test :db-client-java:test --debug
\ No newline at end of file
+    uses: ./.github/workflows/tests.yml
+    with:
+      esdb_version: ${{ matrix.version }}
\ No newline at end of file
diff --git a/db-client-java/build.gradle b/db-client-java/build.gradle
index d1d0648..bc42cc4 100644
--- a/db-client-java/build.gradle
+++ b/db-client-java/build.gradle
@@ -46,29 +46,93 @@ dependencies {
     implementation "io.grpc:grpc-stub:${grpcVersion}"
     implementation "io.grpc:grpc-protobuf:${grpcVersion}"
     implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2'
-    implementation 'org.slf4j:slf4j-api:1.7.36'
+    implementation 'org.slf4j:slf4j-api:2.0.7'
 
     testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
     testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
     testImplementation "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
     testImplementation "org.junit.platform:junit-platform-suite:1.8.2"
+    testImplementation 'org.junit-pioneer:junit-pioneer:1.9.1'
     testImplementation 'io.reactivex.rxjava3:rxjava:3.1.3'
     testImplementation "org.reactivestreams:reactive-streams-tck:${reactiveStreamsApiVersion}"
     testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}"
     testImplementation platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")
     testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
     testImplementation "com.github.javafaker:javafaker:1.0.2"
-    testImplementation 'org.slf4j:slf4j-simple:1.7.36'
+    testImplementation 'org.slf4j:slf4j-simple:2.0.7'
 }
 
-test {
-    useJUnitPlatform()
-
+tasks.withType(Test).configureEach {
     testLogging {
         events "PASSED", "SKIPPED", "FAILED"
+        showExceptions true
+        exceptionFormat "full"
+        showCauses true
+        showStackTraces true
+        showStandardStreams = true
+    }
+}
+
+tasks.register("generateCertificates", Exec) {
+    description = "Generates SSL certificates to start a secured ESDB server"
+    commandLine 'docker', 'compose', '--file', '../configure-tls-for-tests.yml', 'up'
+}
+
+tasks.register("startDockerCompose", Exec) {
+    description = "Starts ESDB cluster"
+    commandLine 'docker', 'compose', '--file', '../docker-compose.yml', 'up', '-d'
+}
+
+tasks.register("stopDockerCompose", Exec) {
+    description = "Stop ESDB cluster"
+    commandLine 'docker', 'compose', '--file', '../docker-compose.yml', 'down'
+}
+
+
+tasks.register("miscTests", Test) {
+    useJUnitPlatform {
+        include("**/MiscTests.class")
+    }
+}
+
+tasks.register("singleNodeTests", Test) {
+    useJUnitPlatform {
+        include("**/StreamsTests.class")
+        include("**/PersistentSubscriptionsTests.class")
+        include("**/ExpectationsTests.class")
+    }
+}
+
+tasks.register("secureNodeTests", Test) {
+    dependsOn generateCertificates
+    environment "SECURE", "true"
+
+    useJUnitPlatform {
+        include("**/StreamsTests.class")
+        include("**/PersistentSubscriptionsTests.class")
     }
 }
 
+tasks.register("clusterTests", Test) {
+    dependsOn startDockerCompose
+    finalizedBy stopDockerCompose
+    environment "SECURE", "true"
+    environment "CLUSTER", "true"
+
+    useJUnitPlatform {
+        include("**/StreamsTests.class")
+        include("**/PersistentSubscriptionsTests.class")
+    }
+}
+
+tasks.register("ci", Test) {
+    useJUnitPlatform()
+}
+
+test {
+    dependsOn miscTests, singleNodeTests, secureNodeTests, clusterTests
+}
+
 protobuf {
     protoc {
         // The artifact spec for the Protobuf Compiler
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/AbstractRegularSubscription.java b/db-client-java/src/main/java/com/eventstore/dbclient/AbstractRegularSubscription.java
index 6590228..3188459 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/AbstractRegularSubscription.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/AbstractRegularSubscription.java
@@ -8,11 +8,14 @@ import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
 import io.grpc.stub.ClientCallStreamObserver;
 import io.grpc.stub.ClientResponseObserver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.validation.constraints.NotNull;
 import java.util.concurrent.CompletableFuture;
 
 abstract class AbstractRegularSubscription {
+    private static Logger logger = LoggerFactory.getLogger(AbstractRegularSubscription.class);
     protected static final StreamsOuterClass.ReadReq.Options.Builder defaultReadOptions;
     protected static final StreamsOuterClass.ReadReq.Options.Builder defaultSubscribeOptions;
 
@@ -86,9 +89,9 @@ abstract class AbstractRegularSubscription {
                     }
 
                     if (_confirmed && !readResp.hasEvent()) {
-                        onError(new IllegalStateException(
+                        logger.warn(
                                 String.format("Confirmed subscription %s received non-{event,checkpoint} variant",
-                                        _subscription.getSubscriptionId())));
+                                        _subscription.getSubscriptionId()));
                         return;
                     }
 
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ClusterInfo.java b/db-client-java/src/main/java/com/eventstore/dbclient/ClusterInfo.java
index c7cb2c0..faf50d0 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/ClusterInfo.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/ClusterInfo.java
@@ -8,7 +8,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
-class ClusterInfo {
+public class ClusterInfo {
     private final List<Member> members;
 
     public ClusterInfo(List<Member> members) {
@@ -39,7 +39,7 @@ class ClusterInfo {
         return new ClusterInfo(members);
     }
 
-    enum MemberState {
+    public enum MemberState {
         INITIALIZING, DISCOVER_LEADER, UNKNOWN, PRE_REPLICA, CATCHING_UP, CLONE,
         FOLLOWER, PRE_LEADER, LEADER, MANAGER, SHUTTING_DOWN, SHUT_DOWN, READ_ONLY_LEADERLESS,
         PRE_READ_ONLY_REPLICA, READ_ONLY_REPLICA, RESIGNING_LEADER;
@@ -81,13 +81,13 @@ class ClusterInfo {
         }
     }
 
-    static class Member {
+    public static class Member {
         private final UUID instanceId;
         private final boolean isAlive;
         private final MemberState state;
         private final InetSocketAddress httpEndpoint;
 
-        Member(UUID instanceId, boolean isAlive, MemberState state, InetSocketAddress httpEndpoint) {
+        public Member(UUID instanceId, boolean isAlive, MemberState state, InetSocketAddress httpEndpoint) {
             this.instanceId = instanceId;
             this.isAlive = isAlive;
             this.state = state;
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientBase.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientBase.java
index e86bf18..0e589f7 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientBase.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientBase.java
@@ -3,16 +3,17 @@ package com.eventstore.dbclient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
-class EventStoreDBClientBase {
+public class EventStoreDBClientBase {
     final Logger logger = LoggerFactory.getLogger(EventStoreDBClientBase.class);
     final private GrpcClient client;
 
-    protected EventStoreDBClientBase(EventStoreDBClientSettings settings) {
+    EventStoreDBClientBase(EventStoreDBClientSettings settings) {
         Discovery discovery;
 
         if (settings.getHosts().length == 1) {
@@ -52,6 +53,10 @@ class EventStoreDBClientBase {
         return this.client.isShutdown();
     }
 
+    public CompletableFuture<Optional<ServerVersion>> getServerVersion() {
+        return client.getServerVersion();
+    }
+
     GrpcClient getGrpcClient() {
         return client;
     }
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBConnectionString.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBConnectionString.java
index 2b0dff0..06031f7 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBConnectionString.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBConnectionString.java
@@ -175,7 +175,7 @@ public final class EventStoreDBConnectionString {
         return this.parseSearchParams(true);
     }
 
-    static Optional<NodePreference> parseNodePreference(String value) {
+    public static Optional<NodePreference> parseNodePreference(String value) {
         switch (value.toLowerCase()) {
             case "leader":
                 return Optional.of(NodePreference.LEADER);
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBPersistentSubscriptionsClient.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBPersistentSubscriptionsClient.java
index d13a7e0..135662e 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBPersistentSubscriptionsClient.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBPersistentSubscriptionsClient.java
@@ -10,10 +10,15 @@ import java.util.function.Function;
  * Represents EventStoreDB client for persistent subscriptions management. A client instance maintains a two-way communication to EventStoreDB.
  * Many threads can use the EventStoreDB client simultaneously, or a single thread can make many asynchronous requests.
  */
-public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClientBase {
+public class EventStoreDBPersistentSubscriptionsClient {
+    private final EventStoreDBClientBase inner;
 
     private EventStoreDBPersistentSubscriptionsClient(EventStoreDBClientSettings settings) {
-        super(settings);
+        inner = new EventStoreDBClientBase(settings);
+    }
+
+    private EventStoreDBPersistentSubscriptionsClient(EventStoreDBClientBase inner) {
+        this.inner = inner;
     }
 
     /**
@@ -23,6 +28,13 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
         return new EventStoreDBPersistentSubscriptionsClient(settings);
     }
 
+    /**
+     * Returns a Persistent Subscription Management client based on existing client.
+     * @param existingClient Existing client.
+     */
+    public static EventStoreDBPersistentSubscriptionsClient from(EventStoreDBClientBase existingClient) {
+        return new EventStoreDBPersistentSubscriptionsClient(existingClient);
+    }
     /**
      * Creates a persistent subscription group on a stream.
      *
@@ -72,7 +84,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = CreatePersistentSubscriptionToStreamOptions.get();
         }
 
-        return new CreatePersistentSubscriptionToStream(this.getGrpcClient(), stream, group, options).execute();
+        return new CreatePersistentSubscriptionToStream(inner.getGrpcClient(), stream, group, options).execute();
     }
 
     /**
@@ -92,7 +104,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = CreatePersistentSubscriptionToAllOptions.get();
         }
 
-        return new CreatePersistentSubscriptionToAll(this.getGrpcClient(), group, options).execute();
+        return new CreatePersistentSubscriptionToAll(inner.getGrpcClient(), group, options).execute();
     }
 
     /**
@@ -123,7 +135,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = UpdatePersistentSubscriptionToStreamOptions.get();
         }
 
-        return new UpdatePersistentSubscriptionToStream(this.getGrpcClient(), stream, group, options).execute();
+        return new UpdatePersistentSubscriptionToStream(inner.getGrpcClient(), stream, group, options).execute();
     }
 
     /**
@@ -136,7 +148,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = UpdatePersistentSubscriptionToAllOptions.get();
         }
 
-        return new UpdatePersistentSubscriptionToAll(this.getGrpcClient(), group, options).execute();
+        return new UpdatePersistentSubscriptionToAll(inner.getGrpcClient(), group, options).execute();
     }
 
     /**
@@ -167,7 +179,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = DeletePersistentSubscriptionOptions.get();
         }
 
-        return new DeletePersistentSubscriptionToStream(this.getGrpcClient(), stream, group, options).execute();
+        return new DeletePersistentSubscriptionToStream(inner.getGrpcClient(), stream, group, options).execute();
     }
 
     /**
@@ -180,7 +192,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = DeletePersistentSubscriptionOptions.get();
         }
 
-        return new DeletePersistentSubscriptionToAll(this.getGrpcClient(), group, options).execute();
+        return new DeletePersistentSubscriptionToAll(inner.getGrpcClient(), group, options).execute();
     }
 
     /**
@@ -217,7 +229,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = SubscribePersistentSubscriptionOptions.get();
         }
 
-        return new SubscribePersistentSubscriptionToStream(this.getGrpcClient(), stream, group, options, listener).execute();
+        return new SubscribePersistentSubscriptionToStream(inner.getGrpcClient(), stream, group, options, listener).execute();
     }
 
     /**
@@ -232,7 +244,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
             options = SubscribePersistentSubscriptionOptions.get();
         }
 
-        return new SubscribePersistentSubscriptionToAll(this.getGrpcClient(), group, options, listener).execute();
+        return new SubscribePersistentSubscriptionToAll(inner.getGrpcClient(), group, options, listener).execute();
     }
 
     /**
@@ -242,7 +254,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
      * @see PersistentSubscriptionInfo
      */
     public CompletableFuture<List<PersistentSubscriptionInfo>> listAll(ListPersistentSubscriptionsOptions options) {
-        return ListPersistentSubscriptions.execute(this.getGrpcClient(), options, "", Function.identity());
+        return ListPersistentSubscriptions.execute(inner.getGrpcClient(), options, "", Function.identity());
     }
 
     /**
@@ -260,7 +272,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
      * @param options list persistent subscriptions request's options.
      */
     public CompletableFuture<List<PersistentSubscriptionToStreamInfo>> listToStream(String stream, ListPersistentSubscriptionsOptions options) {
-        return ListPersistentSubscriptions.execute(this.getGrpcClient(), options, stream, info -> (PersistentSubscriptionToStreamInfo) info);
+        return ListPersistentSubscriptions.execute(inner.getGrpcClient(), options, stream, info -> (PersistentSubscriptionToStreamInfo) info);
     }
 
     /**
@@ -286,7 +298,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
      * @see PersistentSubscriptionToAllInfo
      */
     public CompletableFuture<List<PersistentSubscriptionToAllInfo>> listToAll(ListPersistentSubscriptionsOptions options) {
-        return ListPersistentSubscriptions.execute(this.getGrpcClient(), options, "$all", info -> (PersistentSubscriptionToAllInfo) info);
+        return ListPersistentSubscriptions.execute(inner.getGrpcClient(), options, "$all", info -> (PersistentSubscriptionToAllInfo) info);
     }
 
     /**
@@ -297,7 +309,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
      * @see PersistentSubscriptionInfo
      */
     public CompletableFuture<Optional<PersistentSubscriptionToStreamInfo>> getInfoToStream(String stream, String groupName, GetPersistentSubscriptionInfoOptions options) {
-        return GetPersistentSubscriptionInfo.execute(this.getGrpcClient(), options, stream, groupName).thenApply(res ->
+        return GetPersistentSubscriptionInfo.execute(inner.getGrpcClient(), options, stream, groupName).thenApply(res ->
             res.map(PersistentSubscriptionToStreamInfo.class::cast)
         );
     }
@@ -319,7 +331,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
      * @see PersistentSubscriptionToAllInfo
      */
     public CompletableFuture<Optional<PersistentSubscriptionToAllInfo>> getInfoToAll(String groupName, GetPersistentSubscriptionInfoOptions options) {
-        return GetPersistentSubscriptionInfo.execute(this.getGrpcClient(), options, "$all", groupName).thenApply(res ->
+        return GetPersistentSubscriptionInfo.execute(inner.getGrpcClient(), options, "$all", groupName).thenApply(res ->
                 res.map(PersistentSubscriptionToAllInfo.class::cast)
         );
     }
@@ -340,7 +352,7 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
      * @param options replay parked messages to stream request's options.
      */
     public CompletableFuture replayParkedMessagesToStream(String stream, String groupName, ReplayParkedMessagesOptions options) {
-        return ReplayParkedMessages.execute(this.getGrpcClient(), options, stream, groupName);
+        return ReplayParkedMessages.execute(inner.getGrpcClient(), options, stream, groupName);
     }
 
     /**
@@ -382,6 +394,23 @@ public class EventStoreDBPersistentSubscriptionsClient extends EventStoreDBClien
 
      */
     public CompletableFuture restartSubsystem(RestartPersistentSubscriptionSubsystemOptions options) {
-        return RestartPersistentSubscriptionSubsystem.execute(this.getGrpcClient(), options);
+        return RestartPersistentSubscriptionSubsystem.execute(inner.getGrpcClient(), options);
+    }
+
+    /**
+     * Closes a connection and cleans all its allocated resources.
+     */
+    public CompletableFuture<Void> shutdown() {
+        return inner.shutdown();
+    }
+
+    /**
+     * Checks if this client instance has been shutdown.
+     * After shutdown a client instance can no longer process new operations and
+     * a new client instance has to be created.
+     * @return {@code true} if client instance has been shutdown.
+     */
+    public boolean isShutdown() {
+        return inner.isShutdown();
     }
 }
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBProjectionManagementClient.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBProjectionManagementClient.java
index 6c7e3fc..d8d7db0 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBProjectionManagementClient.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBProjectionManagementClient.java
@@ -11,9 +11,14 @@ import java.util.function.Function;
  * Represents EventStoreDB client for projections management. A client instance maintains a two-way communication to EventStoreDB.
  * Many threads can use the EventStoreDB client simultaneously, or a single thread can make many asynchronous requests.
  */
-public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBase {
+public class EventStoreDBProjectionManagementClient {
+    final EventStoreDBClientBase inner;
     private EventStoreDBProjectionManagementClient(EventStoreDBClientSettings settings) {
-        super(settings);
+        inner = new EventStoreDBClientBase(settings);
+    }
+
+    private EventStoreDBProjectionManagementClient(EventStoreDBClientBase inner) {
+        this.inner = inner;
     }
 
     /**
@@ -24,6 +29,14 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
         return new EventStoreDBProjectionManagementClient(settings);
     }
 
+    /**
+     * Returns a Projection Management client based on existing client.
+     * @param existingClient Existing client.
+     */
+    public static EventStoreDBProjectionManagementClient from(EventStoreDBClientBase existingClient) {
+        return new EventStoreDBProjectionManagementClient(existingClient);
+    }
+
     /**
      * Stops the projection without writing a checkpoint.
      * This can be used to disable a projection that has been faulted.
@@ -40,7 +53,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture abort(final String projectionName, AbortProjectionOptions options) {
-        return new AbortProjection(this.getGrpcClient(), projectionName, options).execute();
+        return new AbortProjection(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -62,7 +75,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
         if (options == null)
             options = CreateProjectionOptions.get();
 
-        return new CreateProjection(this.getGrpcClient(), projectionName, query, options).execute();
+        return new CreateProjection(inner.getGrpcClient(), projectionName, query, options).execute();
     }
 
     /**
@@ -79,7 +92,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture enable(final String projectionName, EnableProjectionOptions options) {
-        return new EnableProjection(this.getGrpcClient(), projectionName, options).execute();
+        return new EnableProjection(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -96,7 +109,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture delete(final String projectionName, DeleteProjectionOptions options) {
-        return new DeleteProjection(this.getGrpcClient(), projectionName, options).execute();
+        return new DeleteProjection(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -113,7 +126,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture disable(final String projectionName, DisableProjectionOptions options) {
-        return new DisableProjection(this.getGrpcClient(), projectionName, options).execute();
+        return new DisableProjection(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -133,7 +146,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param <TResult> The result type to return.
      */
     public <TResult> CompletableFuture<TResult>  getResult(final String projectionName, Class<TResult> type, GetProjectionResultOptions options) {
-        return new GetProjectionResult<>(this.getGrpcClient(), projectionName, options, type).execute();
+        return new GetProjectionResult<>(inner.getGrpcClient(), projectionName, options, type).execute();
     }
 
     /**
@@ -155,7 +168,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      */
     public <TResult> CompletableFuture<TResult> getResult(final String projectionName,
                                                           Function<TypeFactory, JavaType> javaTypeFunction, GetProjectionResultOptions options) {
-        return new GetProjectionResult<TResult>(this.getGrpcClient(), projectionName, options, javaTypeFunction).execute();
+        return new GetProjectionResult<TResult>(inner.getGrpcClient(), projectionName, options, javaTypeFunction).execute();
     }
 
     /**
@@ -175,7 +188,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param <TResult> The result type to return.
      */
     public <TResult> CompletableFuture<TResult> getState(final String projectionName, Class<TResult> type, GetProjectionStateOptions options) {
-        return new GetProjectionState<>(this.getGrpcClient(), projectionName, options, type).execute();
+        return new GetProjectionState<>(inner.getGrpcClient(), projectionName, options, type).execute();
     }
 
     /**
@@ -197,7 +210,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      */
     public <TResult> CompletableFuture<TResult> getState(final String projectionName,
                                                          Function<TypeFactory, JavaType> javaTypeFunction, GetProjectionStateOptions options) {
-        return new GetProjectionState<TResult>(this.getGrpcClient(), projectionName, options, javaTypeFunction).execute();
+        return new GetProjectionState<TResult>(inner.getGrpcClient(), projectionName, options, javaTypeFunction).execute();
     }
 
     /**
@@ -214,7 +227,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture<ProjectionDetails> getStatistics(final String projectionName, GetProjectionStatisticsOptions options) {
-        return new GetProjectionStatistics(this.getGrpcClient(), projectionName, options).execute();
+        return new GetProjectionStatistics(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -231,7 +244,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture<ProjectionDetails> getStatus(final String projectionName, final GetProjectionStatusOptions options) {
-        return new GetProjectionStatus(this.getGrpcClient(), projectionName, options).execute();
+        return new GetProjectionStatus(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -246,7 +259,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture<List<ProjectionDetails>> list(ListProjectionsOptions options) {
-        return new ListProjections(this.getGrpcClient(), options).execute().thenApply(ListProjectionsResult::getProjections);
+        return new ListProjections(inner.getGrpcClient(), options).execute().thenApply(ListProjectionsResult::getProjections);
     }
 
     /**
@@ -265,7 +278,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture reset(final String projectionName, ResetProjectionOptions options) {
-        return new ResetProjection(this.getGrpcClient(), projectionName, options).execute();
+        return new ResetProjection(inner.getGrpcClient(), projectionName, options).execute();
     }
 
     /**
@@ -280,7 +293,7 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture restartSubsystem(RestartProjectionSubsystemOptions options) {
-        return new RestartProjectionSubsystem(this.getGrpcClient(), options).execute();
+        return new RestartProjectionSubsystem(inner.getGrpcClient(), options).execute();
     }
 
     /**
@@ -299,6 +312,23 @@ public class EventStoreDBProjectionManagementClient extends EventStoreDBClientBa
      * @param options Additional options.
      */
     public CompletableFuture update(final String projectionName, final String query, UpdateProjectionOptions options) {
-        return new UpdateProjection(this.getGrpcClient(), projectionName, query, options).execute();
+        return new UpdateProjection(inner.getGrpcClient(), projectionName, query, options).execute();
+    }
+
+    /**
+     * Closes a connection and cleans all its allocated resources.
+     */
+    public CompletableFuture<Void> shutdown() {
+        return inner.shutdown();
+    }
+
+    /**
+     * Checks if this client instance has been shutdown.
+     * After shutdown a client instance can no longer process new operations and
+     * a new client instance has to be created.
+     * @return {@code true} if client instance has been shutdown.
+     */
+    public boolean isShutdown() {
+        return inner.isShutdown();
     }
 }
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ExpectedRevision.java b/db-client-java/src/main/java/com/eventstore/dbclient/ExpectedRevision.java
index d18a605..c04d827 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/ExpectedRevision.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/ExpectedRevision.java
@@ -89,7 +89,7 @@ public abstract class ExpectedRevision {
         return Objects.hash(getClass());
     }
 
-    static class NoStreamExpectedRevision extends ExpectedRevision {
+    public static class NoStreamExpectedRevision extends ExpectedRevision {
         @Override
         public StreamsOuterClass.AppendReq.Options.Builder applyOnWire(StreamsOuterClass.AppendReq.Options.Builder options) {
             return options.setNoStream(Shared.Empty.getDefaultInstance());
@@ -111,7 +111,7 @@ public abstract class ExpectedRevision {
         }
     }
 
-    static class AnyExpectedRevision extends ExpectedRevision {
+    public static class AnyExpectedRevision extends ExpectedRevision {
         @Override
         public StreamsOuterClass.AppendReq.Options.Builder applyOnWire(StreamsOuterClass.AppendReq.Options.Builder options) {
             return options.setAny(Shared.Empty.getDefaultInstance());
@@ -133,7 +133,7 @@ public abstract class ExpectedRevision {
         }
     }
 
-    static class StreamExistsExpectedRevision extends ExpectedRevision {
+    public static class StreamExistsExpectedRevision extends ExpectedRevision {
         @Override
         public StreamsOuterClass.AppendReq.Options.Builder applyOnWire(StreamsOuterClass.AppendReq.Options.Builder options) {
             return options.setStreamExists(Shared.Empty.getDefaultInstance());
@@ -155,7 +155,7 @@ public abstract class ExpectedRevision {
         }
     }
 
-    static class SpecificExpectedRevision extends ExpectedRevision {
+    public static class SpecificExpectedRevision extends ExpectedRevision {
         final long version;
 
         SpecificExpectedRevision(long version) {
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/GossipClient.java b/db-client-java/src/main/java/com/eventstore/dbclient/GossipClient.java
index b84f5dc..177e5b6 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/GossipClient.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/GossipClient.java
@@ -21,7 +21,7 @@ class GossipClient {
 
     public GossipClient(EventStoreDBClientSettings settings, ManagedChannel channel) {
         _channel = channel;
-        _stub = GrpcUtils.configureStub(GossipGrpc.newStub(_channel), settings, new GossipOption());
+        _stub = GrpcUtils.configureStub(GossipGrpc.newStub(_channel), settings, new GossipOption(), (long)settings.getGossipTimeout());
     }
 
     public void shutdown() {
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/GrpcUtils.java b/db-client-java/src/main/java/com/eventstore/dbclient/GrpcUtils.java
index 58c1e50..db8e570 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/GrpcUtils.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/GrpcUtils.java
@@ -113,13 +113,19 @@ final class GrpcUtils {
     }
 
     static public <S extends AbstractAsyncStub<S>, O> S configureStub(S stub, EventStoreDBClientSettings settings, OptionsBase<O> options) {
+        return configureStub(stub, settings, options, null);
+    }
+
+    static public <S extends AbstractAsyncStub<S>, O> S configureStub(S stub, EventStoreDBClientSettings settings, OptionsBase<O> options, Long forceDeadlineInMs) {
         S finalStub = stub;
         ConnectionMetadata metadata = new ConnectionMetadata();
 
         if (options.getKind() != OperationKind.Streaming) {
             long deadlineInMs = 10_000;
 
-            if (options.getDeadline() != null) {
+            if (forceDeadlineInMs != null) {
+               deadlineInMs = forceDeadlineInMs;
+            } else if (options.getDeadline() != null) {
                 deadlineInMs = options.getDeadline();
             } else if (settings.getDefaultDeadline() != null) {
                 deadlineInMs = settings.getDefaultDeadline();
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/NodeSelector.java b/db-client-java/src/main/java/com/eventstore/dbclient/NodeSelector.java
index 26adffa..25768cd 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/NodeSelector.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/NodeSelector.java
@@ -2,7 +2,7 @@ package com.eventstore.dbclient;
 
 import java.util.*;
 
-class NodeSelector {
+public class NodeSelector {
 
     private static final Random random = new Random();
     private static final Set<ClusterInfo.MemberState> invalidStates;
@@ -33,11 +33,11 @@ class NodeSelector {
 
     private final Comparator<ClusterInfo.Member> memberComparator;
 
-    NodeSelector(NodePreference nodePreference) {
+    public NodeSelector(NodePreference nodePreference) {
         this.memberComparator = memberComparators.get(nodePreference);
     }
 
-    Optional<ClusterInfo.Member> determineBestFitNode(ClusterInfo clusterInfo) {
+    public Optional<ClusterInfo.Member> determineBestFitNode(ClusterInfo clusterInfo) {
         return clusterInfo.getMembers()
                 .stream()
                 .filter(ClusterInfo.Member::isAlive)
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/Position.java b/db-client-java/src/main/java/com/eventstore/dbclient/Position.java
index 70861b6..da00b19 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/Position.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/Position.java
@@ -21,7 +21,7 @@ public class Position implements Comparable<Position> {
         this.commit = commitUnsigned;
     }
 
-    Position(String prepare, String commit) {
+    public Position(String prepare, String commit) {
         this(Long.parseUnsignedLong(commit), Long.parseUnsignedLong(prepare));
     }
 
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ServerVersion.java b/db-client-java/src/main/java/com/eventstore/dbclient/ServerVersion.java
index 2dd3896..46086a3 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/ServerVersion.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/ServerVersion.java
@@ -1,5 +1,5 @@
 package com.eventstore.dbclient;
-class ServerVersion {
+public class ServerVersion {
     private final int major;
     private final int minor;
     private final int patch;
@@ -47,6 +47,17 @@ class ServerVersion {
         return false;
     }
 
+    public boolean isLessOrEqualThan(int major, int minor) {
+        int cmp;
+        if ((cmp = Integer.compare(this.major, major)) != 0)
+            return cmp < 0;
+
+        if ((cmp = Integer.compare(this.minor, minor)) != 0)
+            return cmp < 0;
+
+        return true;
+    }
+
     @Override
     public String toString() {
         return "ServerVersion{" +
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java b/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java
index e114a69..a1a4ca1 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java
@@ -108,7 +108,7 @@ public class StreamMetadata {
         }
     }
 
-    Object serialize() {
+    public Object serialize() {
         HashMap<String, Object> output = new HashMap<>();
 
         insertValue(output, "$maxAge", this.maxAge);
@@ -132,7 +132,7 @@ public class StreamMetadata {
         return output;
     }
 
-     static StreamMetadata deserialize(HashMap<String, Object> source) {
+     public static StreamMetadata deserialize(HashMap<String, Object> source) {
         StreamMetadata metadata = new StreamMetadata();
         HashMap<String, Object> customProperties = null;
 
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..8835cdd
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,106 @@
+version: '3.5'
+
+services:
+  volumes-provisioner:
+    image: "hasnat/volumes-provisioner"
+    environment:
+      PROVISION_DIRECTORIES: "1000:1000:0755:/tmp/certs"
+    volumes:
+      - "./certs:/tmp/certs"
+    network_mode: "none"
+
+  setup:
+    image: eventstore/es-gencert-cli:1.0.2
+    entrypoint: bash
+    user: "1000:1000"
+    command: >
+      -c "mkdir -p ./certs && cd /certs
+      && es-gencert-cli create-ca
+      && es-gencert-cli create-node -out ./node1 --dns-names node1.eventstore
+      && es-gencert-cli create-node -out ./node2 --dns-names node2.eventstore
+      && es-gencert-cli create-node -out ./node3 --dns-names node3.eventstore
+      && find . -type f -print0 | xargs -0 chmod 666"
+    container_name: setup
+    volumes:
+      - ./certs:/certs
+    depends_on:
+      - volumes-provisioner
+
+  node1.eventstore: &template
+    image: ghcr.io/eventstore/eventstore:${CONTAINER_IMAGE_VERSION:-latest}
+    container_name: node1.eventstore
+    env_file:
+      - vars.env
+    environment:
+      - EVENTSTORE_EXT_HOST_ADVERTISE_AS=node1.eventstore
+      - EVENTSTORE_INT_HOST_ADVERTISE_AS=node1.eventstore
+      - EVENTSTORE_GOSSIP_SEED=node2.eventstore:2113,node3.eventstore:2113
+      - EVENTSTORE_CERTIFICATE_FILE=/certs/node1/node.crt
+      - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/certs/node1/node.key
+      - EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS=2111
+      - EVENTSTORE_ADVERTISE_TCP_PORT_TO_CLIENT_AS=1111
+    healthcheck:
+      test:
+        [
+            'CMD-SHELL',
+            'curl --fail --insecure https://node1.eventstore:2113/health/live || exit 1',
+        ]
+      interval: 5s
+      timeout: 5s
+      retries: 24
+    ports:
+      - 1111:1113
+      - 2111:2113
+    volumes:
+      - ./certs:/certs
+    depends_on:
+      - setup
+    restart: always
+
+  node2.eventstore:
+    <<: *template
+    container_name: node2.eventstore
+    environment:
+      - EVENTSTORE_EXT_HOST_ADVERTISE_AS=node2.eventstore
+      - EVENTSTORE_INT_HOST_ADVERTISE_AS=node2.eventstore
+      - EVENTSTORE_GOSSIP_SEED=node1.eventstore:2113,node3.eventstore:2113
+      - EVENTSTORE_CERTIFICATE_FILE=/certs/node2/node.crt
+      - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/certs/node2/node.key
+      - EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS=2112
+      - EVENTSTORE_ADVERTISE_TCP_PORT_TO_CLIENT_AS=1112
+    healthcheck:
+      test:
+        [
+            'CMD-SHELL',
+            'curl --fail --insecure https://node2.eventstore:2113/health/live || exit 1',
+        ]
+      interval: 5s
+      timeout: 5s
+      retries: 24
+    ports:
+      - 1112:1113
+      - 2112:2113
+
+  node3.eventstore:
+    <<: *template
+    container_name: node3.eventstore
+    environment:
+      - EVENTSTORE_EXT_HOST_ADVERTISE_AS=node3.eventstore
+      - EVENTSTORE_INT_HOST_ADVERTISE_AS=node3.eventstore
+      - EVENTSTORE_GOSSIP_SEED=node1.eventstore:2113,node2.eventstore:2113
+      - EVENTSTORE_CERTIFICATE_FILE=/certs/node3/node.crt
+      - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/certs/node3/node.key
+      - EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS=2113
+      - EVENTSTORE_ADVERTISE_TCP_PORT_TO_CLIENT_AS=1113
+    healthcheck:
+      test:
+        [
+            'CMD-SHELL',
+            'curl --fail --insecure https://node3.eventstore:2113/health/live || exit 1',
+        ]
+      interval: 5s
+      timeout: 5s
+      retries: 24
+    ports:
+      - 1113:1113
+      - 2113:2113
diff --git a/gradle.properties b/gradle.properties
index 758c25a..2c0bcfd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,5 +11,5 @@ junitVersion=5.8.2
 
 # Test Dependencies
 slf4jNopVersion=1.7.29
-testcontainersVersion=1.16.3
+testcontainersVersion=1.18.3
 jacksonVersion=2.11.1
diff --git a/vars.env b/vars.env
new file mode 100644
index 0000000..ff780c8
--- /dev/null
+++ b/vars.env
@@ -0,0 +1,7 @@
+EVENTSTORE_CLUSTER_SIZE=3
+EVENTSTORE_RUN_PROJECTIONS=All
+EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/certs/ca
+EVENTSTORE_DISCOVER_VIA_DNS=false
+EVENTSTORE_ENABLE_EXTERNAL_TCP=true
+EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true
+EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS=localhost
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/test-dispatch.yml b/.github/workflows/test-dispatch.yml
new file mode 100644
index 0000000..3aab752
--- /dev/null
+++ b/.github/workflows/test-dispatch.yml
@@ -0,0 +1,16 @@
+name: "Dispatch"
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "Version tag"
+        required: true
+        type: string
+
+jobs:
+  test:
+    name: Test
+    uses: ./.github/workflows/tests.yml
+    with:
+      esdb_version: ${{ inputs.version }}
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..5a21257
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,120 @@
+name: tests workflow
+
+on:
+  workflow_call:
+    inputs:
+      esdb_version:
+        required: true
+        type: string
+
+jobs:
+  single_node:
+    name: Single node
+
+    strategy:
+      fail-fast: false
+      matrix:
+        test: [Streams, PersistentSubscriptions, Expectations ]
+
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up JDK 8
+        uses: actions/setup-java@v3
+        with:
+          java-version: '8'
+          distribution: 'temurin'
+
+      - name: Setup Gradle
+        uses: gradle/gradle-build-action@v2
+        with:
+          gradle-version: 7.4
+
+      - name: Execute Gradle build
+        run: ./gradlew ci --tests ${{ matrix.test }}Tests
+        env:
+          EVENTSTORE_DOCKER_TAG_ENV: ${{ inputs.esdb_version }}
+
+      - uses: actions/upload-artifact@v3
+        if: failure()
+        with:
+          name: esdb_logs.tar.gz
+          path: db-client-java/esdb_logs.tar.gz
+          if-no-files-found: error
+
+  secure:
+    name: Secure
+
+    strategy:
+      fail-fast: false
+      matrix:
+        test: [Streams, PersistentSubscriptions]
+
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Generate certificates
+        run: docker-compose --file configure-tls-for-tests.yml up
+
+      - name: Set up JDK 8
+        uses: actions/setup-java@v3
+        with:
+          java-version: '8'
+          distribution: 'temurin'
+
+      - name: Setup Gradle
+        uses: gradle/gradle-build-action@v2
+        with:
+          gradle-version: 7.4
+
+      - name: Execute Gradle build
+        run: ./gradlew ci --tests ${{ matrix.test }}Tests
+        env:
+          EVENTSTORE_DOCKER_TAG_ENV: ${{ inputs.esdb_version }}
+          SECURE: true
+
+      - uses: actions/upload-artifact@v3
+        if: failure()
+        with:
+          name: esdb-logs
+          path: eventstore/esdb_logs
+
+  cluster:
+    name: Cluster
+
+    strategy:
+      fail-fast: false
+      matrix:
+        test: [Streams, PersistentSubscriptions]
+
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up cluster with Docker Compose
+        run: docker-compose up -d
+        env:
+          CONTAINER_IMAGE_VERSION: ${{ inputs.esdb_version }}
+
+      - name: Set up JDK 8
+        uses: actions/setup-java@v3
+        with:
+          java-version: '8'
+          distribution: 'temurin'
+
+      - name: Setup Gradle
+        uses: gradle/gradle-build-action@v2
+        with:
+          gradle-version: 7.4
+
+      - name: Execute Gradle build
+        run: ./gradlew ci --tests ${{ matrix.test }}Tests
+        env:
+          EVENTSTORE_DOCKER_TAG_ENV: ${{ inputs.esdb_version }}
+          CLUSTER: true
+          SECURE: true
+
+      - name: Shutdown cluster
+        run: docker-compose down
diff --git a/configure-tls-for-tests.yml b/configure-tls-for-tests.yml
new file mode 100644
index 0000000..62b8728
--- /dev/null
+++ b/configure-tls-for-tests.yml
@@ -0,0 +1,26 @@
+version: '3.5'
+
+services:
+  volumes-provisioner:
+    image: "hasnat/volumes-provisioner"
+    environment:
+      PROVISION_DIRECTORIES: "1000:1000:0755:/tmp/certs"
+    volumes:
+      - "./certs:/tmp/certs"
+    network_mode: "none"
+
+  setup:
+    image: eventstore/es-gencert-cli:1.0.2
+    entrypoint: bash
+    user: "1000:1000"
+    command: >
+      -c "mkdir -p ./certs && cd /certs
+      && es-gencert-cli create-ca
+      && es-gencert-cli create-node -out ./node --dns-names localhost
+      && es-gencert-cli create-ca -out ./untrusted-ca
+      && find . -type f -print0 | xargs -0 chmod 666"
+    container_name: setup
+    volumes:
+      - ./certs:/certs
+    depends_on:
+      - volumes-provisioner
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/Action.java b/db-client-java/src/test/java/com/eventstore/dbclient/Action.java
new file mode 100644
index 0000000..b132cd1
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/Action.java
@@ -0,0 +1,6 @@
+package com.eventstore.dbclient;
+
+@FunctionalInterface
+public interface Action<A> {
+    A run() throws Exception;
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/BazEvent.java b/db-client-java/src/test/java/com/eventstore/dbclient/BazEvent.java
new file mode 100644
index 0000000..bc7f174
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/BazEvent.java
@@ -0,0 +1,44 @@
+package com.eventstore.dbclient;
+
+import java.util.Objects;
+
+public class BazEvent {
+    private String name;
+    private int age;
+
+    public BazEvent() {}
+
+    public BazEvent(String name, int age) {
+        this.name = name;
+        this.age = age;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getAge() {
+        return age;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setAge(int age) {
+        this.age = age;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        BazEvent bazEvent = (BazEvent) o;
+        return age == bazEvent.age && Objects.equals(name, bazEvent.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, age);
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ClientTracker.java b/db-client-java/src/test/java/com/eventstore/dbclient/ClientTracker.java
new file mode 100644
index 0000000..d45d8d1
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/ClientTracker.java
@@ -0,0 +1,106 @@
+package com.eventstore.dbclient;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class ClientTracker {
+    private static final Logger logger = LoggerFactory.getLogger(ClientTracker.class);
+    private ArrayList<EventStoreDBClientBase> otherClients = new ArrayList<>();
+    private EventStoreDBClient defaultClient = null;
+
+    public synchronized EventStoreDBClient createClient(EventStoreDBClientSettings settings) {
+        EventStoreDBClient client = EventStoreDBClient.create(settings);
+        otherClients.add(client);
+        return client;
+    }
+
+    public synchronized EventStoreDBClient getDefaultClient(Database database) {
+        if (defaultClient == null) {
+            EventStoreDBClientSettings settings = database.defaultSettingsBuilder().buildConnectionSettings();
+            defaultClient = EventStoreDBClient.create(settings);
+
+            if (settings.isTls() && settings.getDefaultCredentials() != null) {
+                for (int count = 0; count < 50; count++) {
+                    logger.debug(String.format("Checking if admin user is available...%d/50", count));
+                    try {
+                        defaultClient.readStream("$users", ReadStreamOptions.get()).get(1, TimeUnit.SECONDS);
+                        logger.debug("Admin account is available!");
+                        break;
+                    } catch (InterruptedException | TimeoutException e) {
+                        if (e instanceof TimeoutException) {
+                            logger.debug("Request timed out, retrying...");
+                            try {
+                                Thread.sleep(500);
+                            } catch (InterruptedException ex) {
+                                throw new RuntimeException(ex);
+                            }
+                        } else
+                            throw new RuntimeException(e);
+
+                    } catch (ExecutionException e) {
+                        if (e.getCause() instanceof StatusRuntimeException) {
+                            StatusRuntimeException grpcExc = (StatusRuntimeException) e.getCause();
+                            Status.Code code = grpcExc.getStatus().getCode();
+
+                            if (code == Status.Code.PERMISSION_DENIED
+                                    || code == Status.Code.UNAUTHENTICATED
+                                    || code == Status.Code.DEADLINE_EXCEEDED
+                                    || code == Status.Code.NOT_FOUND
+                                    || code == Status.Code.UNAVAILABLE) {
+                                logger.debug("Admin not available, retrying", grpcExc);
+                                try {
+                                    Thread.sleep(500);
+                                } catch (InterruptedException ex) {
+                                    throw new RuntimeException(ex);
+                                }
+                                continue;
+                            }
+                        }
+
+                        if (e.getCause() instanceof NotLeaderException) {
+                            logger.debug("Can't access admin because of no leader exception error, retrying...");
+                            try {
+                                Thread.sleep(500);
+                            } catch (InterruptedException ex) {
+                                throw new RuntimeException(ex);
+                            }
+                            continue;
+                        }
+
+                        throw new RuntimeException(e);
+                    }
+                }
+            }
+        }
+
+        return defaultClient;
+    }
+
+    public void dispose() {
+        if (defaultClient != null) {
+            try {
+                defaultClient.shutdown().get();
+            } catch (ExecutionException | InterruptedException e) {
+                logger.error("Error when shutting down client", e);
+            }
+            defaultClient = null;
+        }
+
+        for (EventStoreDBClientBase client: otherClients) {
+            try {
+                client.shutdown().get();
+            } catch (ExecutionException | InterruptedException e) {
+                logger.error("Error when shutting down client", e);
+            }
+        }
+
+        otherClients = new ArrayList<>();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ConnectionAware.java b/db-client-java/src/test/java/com/eventstore/dbclient/ConnectionAware.java
new file mode 100644
index 0000000..ee1773d
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/ConnectionAware.java
@@ -0,0 +1,115 @@
+package com.eventstore.dbclient;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.github.javafaker.Faker;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public interface ConnectionAware {
+    Faker faker = new Faker();
+    JsonMapper mapper = new JsonMapper();
+
+    Database getDatabase();
+
+    Logger getLogger();
+
+    default EventStoreDBClient getDefaultClient() {
+        return getDatabase().defaultClient();
+    }
+
+    default EventStoreDBProjectionManagementClient getDefaultProjectionClient() {
+        return EventStoreDBProjectionManagementClient.from(getDefaultClient());
+    }
+
+    default EventStoreDBPersistentSubscriptionsClient getDefaultPersistentSubscriptionClient() {
+        return EventStoreDBPersistentSubscriptionsClient.from(getDefaultClient());
+    }
+
+    default String generateName() {
+        return String.format("%s-%s", faker.pokemon().name(), faker.pokemon().name());
+    }
+
+    default ArrayList<EventData> generateEvents(int amount, String eventType) {
+        ArrayList<EventData> events = new ArrayList<>();
+        for (int i = 0; i < amount; i++) {
+            events.add(EventDataBuilder.json(UUID.randomUUID(), eventType, new byte[]{}).build());
+        }
+
+        return events;
+    }
+
+    default ArrayList<BazEvent> generateBazEvent(int count) {
+        ArrayList<BazEvent> events = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            BazEvent event = new BazEvent(generateName(), faker.number().randomDigit());
+            events.add(event);
+        }
+
+        return events;
+    }
+
+    default EventData serializeBazEvent(BazEvent event) throws JsonProcessingException {
+        return serializeBazEvent(event, null);
+    }
+
+    default EventData serializeBazEvent(BazEvent event, String eventType) throws JsonProcessingException {
+        String type = eventType == null ? generateName() : eventType;
+        return EventDataBuilder.json(type, mapper.writeValueAsBytes(event)).build();
+    }
+
+    default ArrayList<EventData> serializeBazEvents(Collection<BazEvent> events) throws JsonProcessingException {
+        return serializeBazEvents(events, null);
+    }
+
+    default ArrayList<EventData> serializeBazEvents(Collection<BazEvent> events, String eventStype) throws JsonProcessingException {
+        ArrayList<EventData> data = new ArrayList<>();
+
+        for(BazEvent event : events) {
+            data.add(serializeBazEvent(event, eventStype));
+        }
+
+        return data;
+    }
+
+    default BazEvent deserializeBazEvent(byte[] data) throws IOException {
+        return mapper.readValue(data, BazEvent.class);
+    }
+
+    default <A> A flaky(int retries, Exceptions exceptions, Action<A> action) throws Exception {
+        Throwable last = null;
+
+        for (int i = 0; i < retries; i++) {
+            try {
+                return action.run();
+            } catch (Exception e) {
+                if (e instanceof ExecutionException) {
+                    ExecutionException ex = (ExecutionException) e;
+                    if (exceptions.contains(ex.getCause())) {
+                        last = ex.getCause();
+                        Thread.sleep(500);
+                    }
+                    else
+                        throw e;
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        if (last != null)
+            throw new Exception(last);
+        else
+            throw new IllegalStateException("flaky method entered an illegal state");
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/CreatePersistentSubscriptionTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/CreatePersistentSubscriptionTests.java
deleted file mode 100644
index a7f8f0f..0000000
--- a/db-client-java/src/test/java/com/eventstore/dbclient/CreatePersistentSubscriptionTests.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.eventstore.dbclient;
-
-import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
-
-import java.util.concurrent.ExecutionException;
-
-public class CreatePersistentSubscriptionTests extends ESDBTests {
-    @Test
-    public void testCreatePersistentSub() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-
-        client.createToStream(generateName(), generateName())
-                .get();
-
-        client.createToStream(generateName(), generateName(), CreatePersistentSubscriptionToStreamOptions.get().startFrom(2))
-                .get();
-    }
-
-    @Test
-    public void testCreatePersistentSubToAll() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-
-        try {
-            client.createToAll(generateName())
-                    .get();
-
-
-            client.createToAll(generateName(), CreatePersistentSubscriptionToAllOptions.get().startFrom(1, 2))
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-        }
-    }
-}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/Database.java b/db-client-java/src/test/java/com/eventstore/dbclient/Database.java
new file mode 100644
index 0000000..48eaec9
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/Database.java
@@ -0,0 +1,50 @@
+package com.eventstore.dbclient;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+
+public interface Database {
+    Logger logger = LoggerFactory.getLogger(Database.class);
+
+    ConnectionSettingsBuilder defaultSettingsBuilder();
+
+    ClientTracker getClientTracker();
+
+    void cleanup();
+
+    default EventStoreDBClient newClient() {
+        return connectWith(Function.identity());
+    }
+
+    default EventStoreDBClient connectWith(Function<ConnectionSettingsBuilder, ConnectionSettingsBuilder> mod) {
+        return createClient(mod.apply(defaultSettingsBuilder()).buildConnectionSettings());
+    }
+
+    default EventStoreDBClient defaultClient() {
+        return getClientTracker().getDefaultClient(this);
+    }
+
+    default EventStoreDBClient createClient(EventStoreDBClientSettings settings) {
+        return getClientTracker().createClient(settings);
+    }
+
+    default boolean isTargetingBelowOrEqual21_10() {
+        try {
+            Optional<ServerVersion> result = defaultClient().getServerVersion().get();
+            return !result.isPresent() || result.get().isLessOrEqualThan(21, 10);
+        } catch (Exception e) {
+            logger.error("Error when retrieving the server version", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    default void dispose() {
+        cleanup();
+        getClientTracker().dispose();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/DatabaseFactory.java b/db-client-java/src/test/java/com/eventstore/dbclient/DatabaseFactory.java
new file mode 100644
index 0000000..58d3049
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/DatabaseFactory.java
@@ -0,0 +1,33 @@
+package com.eventstore.dbclient;
+
+import com.eventstore.dbclient.databases.DockerContainerDatabase;
+import com.eventstore.dbclient.databases.ExternallyCreatedCluster;
+
+import java.util.Optional;
+
+public class DatabaseFactory {
+    public static Database spawn() {
+        String version = Optional.ofNullable(System.getenv("EVENTSTORE_DOCKER_TAG_ENV")).orElse("latest");
+        boolean secure = Boolean.parseBoolean(Optional.ofNullable(System.getenv("SECURE")).orElse("false"));
+        boolean cluster = Boolean.parseBoolean(Optional.ofNullable(System.getenv("CLUSTER")).orElse("false"));
+
+        if (cluster)
+            return new ExternallyCreatedCluster(secure);
+
+        return DockerContainerDatabase
+                .builder()
+                .version(version)
+                .secure(secure)
+                .build();
+    }
+
+    public static Database spawnPopulatedDatabase() {
+        String version = Optional.ofNullable(System.getenv("EVENTSTORE_DOCKER_TAG_ENV")).orElse("latest");
+
+        return DockerContainerDatabase
+                .builder()
+                .grpcTestDataImage()
+                .version(version)
+                .build();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/DeletePersistentSubscriptionToStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/DeletePersistentSubscriptionToStreamTests.java
deleted file mode 100644
index bc4d17d..0000000
--- a/db-client-java/src/test/java/com/eventstore/dbclient/DeletePersistentSubscriptionToStreamTests.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.eventstore.dbclient;
-
-import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
-
-import java.util.concurrent.ExecutionException;
-
-public class DeletePersistentSubscriptionToStreamTests extends ESDBTests {
-    @Test
-    public void testDeletePersistentSub() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String streamName = generateName();
-        String groupName = generateName();
-
-        client.createToStream(streamName, groupName)
-                .get();
-
-        client.deleteToStream(streamName, groupName)
-                .get();
-    }
-
-    @Test
-    public void testDeletePersistentSubToAll() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String groupName = generateName();
-
-        try {
-            client.createToAll(groupName)
-                    .get();
-
-            client.deleteToAll(groupName)
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-        }
-    }
-}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/Exceptions.java b/db-client-java/src/test/java/com/eventstore/dbclient/Exceptions.java
new file mode 100644
index 0000000..42a5944
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/Exceptions.java
@@ -0,0 +1,52 @@
+package com.eventstore.dbclient;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+public class Exceptions {
+    final private ArrayList<Predicate<Throwable>> exceptions;
+
+    public Exceptions() {
+        exceptions = new ArrayList<>();
+    }
+
+    public <A extends Throwable> Exceptions register(Predicate<A> predicate) {
+        this.exceptions.add(e -> {
+            @SuppressWarnings("unchecked")
+            A target = (A) e;
+            return target != null && predicate.test(target);
+        });
+
+        return this;
+    }
+
+    public <A extends Throwable> Exceptions register(Class<A> clazz) {
+        this.exceptions.add(clazz::isInstance);
+        return this;
+    }
+
+    public Exceptions registerGoAwayError() {
+        return this.<StatusRuntimeException>register(e ->
+            e.getStatus().getCode() == Status.Code.INTERNAL
+        );
+    }
+
+    public Exceptions registerUnknownError() {
+        return this.<StatusRuntimeException>register(e ->
+                e.getStatus().getCode() == Status.Code.UNKNOWN
+        );
+    }
+
+    public boolean contains(Throwable target) {
+        for (Predicate<Throwable> predicate : this.exceptions) {
+            if (predicate.test(target)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ExpectationsTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/ExpectationsTests.java
new file mode 100644
index 0000000..117a922
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/ExpectationsTests.java
@@ -0,0 +1,40 @@
+package com.eventstore.dbclient;
+
+import com.eventstore.dbclient.expectations.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ExpectationsTests implements
+        ReadStreamTests,
+        ReadAllTests,
+        ReadAllReactiveTests,
+        SubscribeToAllTests,
+        ReadStreamReactiveTests,
+        SubscribeToStreamTests,
+        ProjectionManagementTests {
+    static private Database database;
+    static private Logger logger;
+
+    @BeforeAll
+    public static void setup() {
+        database = DatabaseFactory.spawnPopulatedDatabase();
+        logger = LoggerFactory.getLogger(ExpectationsTests.class);
+    }
+
+    @Override
+    public Database getDatabase() {
+        return database;
+    }
+
+    @Override
+    public Logger getLogger() {
+        return logger;
+    }
+
+    @AfterAll
+    public static void cleanup() {
+        database.dispose();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/Foo.java b/db-client-java/src/test/java/com/eventstore/dbclient/Foo.java
new file mode 100644
index 0000000..7e55999
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/Foo.java
@@ -0,0 +1,29 @@
+package com.eventstore.dbclient;
+
+import java.util.Objects;
+
+public class Foo {
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Foo foo1 = (Foo) o;
+        return foo == foo1.foo;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(foo);
+    }
+
+    private boolean foo;
+
+    public boolean isFoo() {
+        return foo;
+    }
+
+    public void setFoo(boolean foo) {
+        this.foo = foo;
+    }
+}
+
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/MiscTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/MiscTests.java
new file mode 100644
index 0000000..5adfd47
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/MiscTests.java
@@ -0,0 +1,8 @@
+package com.eventstore.dbclient;
+
+import org.junit.platform.suite.api.SelectPackages;
+import org.junit.platform.suite.api.Suite;
+
+@Suite
+@SelectPackages("com.eventstore.dbclient.misc")
+public class MiscTests {}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionManagementTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionManagementTests.java
deleted file mode 100644
index e9d1751..0000000
--- a/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionManagementTests.java
+++ /dev/null
@@ -1,351 +0,0 @@
-package com.eventstore.dbclient;
-
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
-import org.junit.jupiter.api.*;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-
-@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-@SuppressWarnings("unchecked")
-public class PersistentSubscriptionManagementTests extends ESDBTests {
-    @Test
-    @Order(1)
-    public void testListPersistentSubscriptions() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String groupName = generateName();
-        String streamA = generateName();
-        String streamB = generateName();
-
-        client.createToStream(streamA, groupName)
-                .get();
-
-        client.createToStream(streamB, groupName)
-                .get();
-
-        List<PersistentSubscriptionInfo> subs = client.listAll().get();
-
-        int count = 0;
-        for (PersistentSubscriptionInfo info: subs) {
-            if (info.getEventSource().equals(streamA) || info.getEventSource().equals(streamB)) {
-                count++;
-            }
-        }
-
-        Assertions.assertEquals(2, count);
-    }
-
-    @Test
-    @Order(2)
-    public void testListPersistentSubscriptionsForStream() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String streamName = generateName();
-        String groupName = generateName();
-
-        client.createToStream(streamName, groupName)
-                .get();
-
-        for (int i = 0; i < 10; i++) {
-            List<PersistentSubscriptionToStreamInfo> subs;
-
-            try {
-                subs = client.listToStream(streamName).get();
-            } catch (ResourceNotFoundException e) {
-                Thread.sleep(500);
-                continue;
-            } catch (ExecutionException e) {
-                if (e.getCause() instanceof StatusRuntimeException) {
-                    StatusRuntimeException status = (StatusRuntimeException) e.getCause();
-                    if (status.getStatus().getCode() == Status.Code.INTERNAL) {
-                        // Some weird use-case when running in Github Action, it's usually a retryable error from the gRPC
-                        // implementation: "Abrupt GOAWAY closed sent stream. HTTP/2 error code: STREAM_CLOSED"
-                        Thread.sleep(500);
-                        continue;
-                    }
-                }
-
-                throw e;
-            }
-
-            Assertions.assertEquals(subs.size(), 1);
-            Assertions.assertEquals(subs.get(0).getEventSource(), streamName);
-            break;
-        }
-    }
-
-    @Test
-    @Order(3)
-    public void testListPersistentSubscriptionsToAll() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String groupName = generateName();
-        try {
-            client.createToAll(groupName)
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-
-            return;
-        }
-
-        List<PersistentSubscriptionToAllInfo> subs = client.listToAll().get();
-
-        Assertions.assertTrue(subs.size() >= 1);
-
-        boolean found = false;
-
-        for (PersistentSubscriptionToAllInfo info : subs) {
-            if (info.getEventSource().equals("$all") && info.getGroupName().equals(groupName)) {
-                found = true;
-                break;
-            }
-        }
-
-        Assertions.assertTrue(found);
-    }
-
-    @Test
-    @Order(4)
-    public void testGetPersistentSubscriptionInfo() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String streamName = generateName();
-        String groupName = generateName();
-        client.createToStream(streamName, groupName)
-                .get();
-        Optional<PersistentSubscriptionToStreamInfo> result = Optional.empty();
-        for (int i = 0; i < 10; i++) {
-            result = client.getInfoToStream(streamName, groupName).get();
-            if (!result.isPresent()) {
-                Thread.sleep(1000);
-                continue;
-            }
-
-            break;
-        }
-
-        Assertions.assertTrue(result.isPresent());
-        Assertions.assertEquals(streamName, result.get().getEventSource());
-    }
-
-    @Test
-    @Order(5)
-    public void testGetPersistentSubscriptionInfoToAll() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String groupName = generateName();
-        try {
-            client.createToAll(groupName)
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-
-            return;
-        }
-
-        Optional<PersistentSubscriptionToAllInfo> result = client.getInfoToAll(groupName).get();
-
-        Assertions.assertTrue(result.isPresent());
-        Assertions.assertEquals("$all", result.get().getEventSource());
-        Assertions.assertEquals(result.get().getGroupName(), groupName);
-    }
-
-    @Test
-    @Order(6)
-    public void testGetPersistentSubscriptionInfoNotExisting() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        Optional<PersistentSubscriptionToStreamInfo> result = client.getInfoToStream(generateName(), generateName()).get();
-
-        Assertions.assertFalse(result.isPresent());
-    }
-
-    @Test
-    @Order(7)
-    public void testReplayParkedMessages() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        final EventStoreDBClient streamClient = getEmptyServer().getClient();
-        final String streamName = generateName();
-        final String groupName = generateName();
-        client.createToStream(streamName, groupName)
-                .get();
-
-        final CompletableFuture allNacked = new CompletableFuture();
-        final CompletableFuture allReplayed = new CompletableFuture();
-
-        client.subscribeToStream(streamName, groupName, new PersistentSubscriptionListener() {
-            int count = 0;
-            @Override
-            public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) {
-                if (count < 2)
-                    subscription.nack(NackAction.Park, "because reason", event);
-                else
-                    subscription.ack(event);
-
-                count++;
-
-                if (count == 2) {
-                    allNacked.complete(null);
-                }
-
-                if (count >= 4) {
-                    allReplayed.complete(null);
-                    subscription.stop();
-                }
-            }
-        }).get();
-
-        for (int i = 0; i < 2; i++) {
-            EventData data = EventData.builderAsJson("foobar", new Foo()).build();
-            streamClient.appendToStream(streamName, data).get();
-        }
-
-        CompletableFuture.runAsync(() -> {
-            for (int i = 0; i < 10; i++) {
-                try {
-                    Thread.sleep(500);
-                    allNacked.get(10, TimeUnit.SECONDS);
-                    // We give the server some time to park those events.
-                    Thread.sleep(10000);
-                    client.replayParkedMessagesToStream(streamName, groupName).get();
-                    allReplayed.get(10, TimeUnit.SECONDS);
-                    break;
-                } catch (Exception e) {
-                    if (e instanceof ExecutionException) {
-                        ExecutionException ex = (ExecutionException) e;
-                        if (ex.getCause() instanceof StatusRuntimeException) {
-                            StatusRuntimeException grpc = (StatusRuntimeException) ex.getCause();
-                            if (grpc.getStatus().getCode() == Status.Code.INTERNAL) {
-                                continue;
-                            }
-                        }
-
-                        if (ex.getCause() instanceof ResourceNotFoundException) {
-                            continue;
-                        }
-                    }
-                    throw new RuntimeException(e);
-                }
-            }
-
-        }).get(40, TimeUnit.SECONDS);
-    }
-
-    @Test
-    @Order(8)
-    public void testReplayParkedMessagesToAll() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String streamName = generateName();
-        String groupName = generateName();
-        final EventStoreDBClient streamClient = getEmptyServer().getClient();
-
-        try {
-            client.createToAll(groupName)
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-
-            return;
-        }
-
-        final CompletableFuture allNacked = new CompletableFuture();
-        final CompletableFuture allReplayed = new CompletableFuture();
-
-        client.subscribeToAll(groupName, new PersistentSubscriptionListener() {
-            int count = 0;
-            @Override
-            public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) {
-                if (count < 2 && event.getOriginalEvent().getStreamId().equals(streamName))
-                    subscription.nack(NackAction.Park, "because reason", event);
-                else
-                    subscription.ack(event);
-
-                if (event.getOriginalEvent().getStreamId().equals(streamName) || event.getEvent().getStreamId().equals(streamName))
-                count++;
-
-                if (count == 2) {
-                    allNacked.complete(null);
-                }
-
-                if (count >= 4) {
-                    allReplayed.complete(null);
-                    subscription.stop();
-                }
-            }
-        }).get();
-
-        for (int i = 0; i < 2; i++) {
-            EventData data = EventData.builderAsJson("foobar", new Foo()).build();
-            streamClient.appendToStream(streamName, data).get();
-        }
-
-        CompletableFuture.runAsync(() -> {
-            for (int i = 0; i < 10; i++) {
-                try {
-                    Thread.sleep(500);
-                    allNacked.get(10, TimeUnit.SECONDS);
-                    // We give the server some time to park those events.
-                    Thread.sleep(10000);
-                    client.replayParkedMessagesToAll(groupName).get();
-                    allReplayed.get(10, TimeUnit.SECONDS);
-                    break;
-                } catch (Exception e) {
-                    if (e instanceof ExecutionException) {
-                        ExecutionException ex = (ExecutionException) e;
-                        if (ex.getCause() instanceof StatusRuntimeException) {
-                            StatusRuntimeException grpc = (StatusRuntimeException) ex.getCause();
-                            if (grpc.getStatus().getCode() == Status.Code.INTERNAL) {
-                                continue;
-                            }
-                        }
-
-                        if (ex.getCause() instanceof ResourceNotFoundException) {
-                            continue;
-                        }
-                    }
-                    throw new RuntimeException(e);
-                }
-            }
-        }).get(40, TimeUnit.SECONDS);
-    }
-
-    @Test
-    @Order(9)
-    public void testEncoding() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        String streamName = String.format("/foo/%s/stream", generateName());
-        String groupName = String.format("/foo/%s/group", generateName());
-
-        client.createToStream(streamName, groupName).get();
-        Optional<PersistentSubscriptionToStreamInfo> info = Optional.empty();
-        for (int i = 0; i < 10; i++) {
-            info = client.getInfoToStream(streamName, groupName).get();
-
-            if (!info.isPresent()) {
-                Thread.sleep(500);
-                continue;
-            }
-
-            break;
-        }
-
-       Assertions.assertTrue(info.isPresent());
-       Assertions.assertEquals(info.get().getEventSource(), streamName);
-       Assertions.assertEquals(info.get().getGroupName(), groupName);
-    }
-
-    @Test
-    @Order(10)
-    public void testRestartSubsystem() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        client.restartSubsystem().get();
-    }
-}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionsTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionsTests.java
new file mode 100644
index 0000000..9012705
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionsTests.java
@@ -0,0 +1,40 @@
+package com.eventstore.dbclient;
+
+import com.eventstore.dbclient.persistentsubscriptions.*;
+import com.eventstore.dbclient.persistentsubscriptions.PersistentSubscriptionToAllWithFilterTests;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PersistentSubscriptionsTests implements
+        CreatePersistentSubscriptionTests,
+        PersistentSubscriptionManagementTests,
+        DeletePersistentSubscriptionToStreamTests,
+        UpdatePersistentSubscriptionToStreamTests,
+        SubscribePersistentSubscriptionToStreamTests,
+        PersistentSubscriptionToAllWithFilterTests {
+    static private Database database;
+    static private Logger logger;
+
+    @BeforeAll
+    public static void setup() {
+        database = DatabaseFactory.spawn();
+        logger = LoggerFactory.getLogger(PersistentSubscriptionsTests.class);
+    }
+
+    @Override
+    public Database getDatabase() {
+        return database;
+    }
+
+    @Override
+    public Logger getLogger() {
+        return logger;
+    }
+
+    @AfterAll
+    public static void cleanup() {
+        database.dispose();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/StreamsTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/StreamsTests.java
new file mode 100644
index 0000000..481be84
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/StreamsTests.java
@@ -0,0 +1,43 @@
+package com.eventstore.dbclient;
+
+
+import com.eventstore.dbclient.streams.*;
+import com.eventstore.dbclient.streams.ReadStreamTests;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class StreamsTests implements
+        AppendTests,
+        ReadStreamTests,
+        SubscriptionTests,
+        DeleteTests,
+        DeadlineTests,
+        InterceptorTests,
+        MetadataTests,
+        ClientLifecycleTests {
+    static private Database database;
+    static private Logger logger;
+
+    @BeforeAll
+    public static void setup() {
+        database = DatabaseFactory.spawn();
+        logger = LoggerFactory.getLogger(StreamsTests.class);
+    }
+
+    @Override
+    public Database getDatabase() {
+        return database;
+    }
+
+    @Override
+    public Logger getLogger() {
+        return logger;
+    }
+
+    @AfterAll
+    public static void cleanup() {
+        database.dispose();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/databases/DockerContainerDatabase.java b/db-client-java/src/test/java/com/eventstore/dbclient/databases/DockerContainerDatabase.java
new file mode 100644
index 0000000..733811a
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/databases/DockerContainerDatabase.java
@@ -0,0 +1,151 @@
+package com.eventstore.dbclient.databases;
+
+import com.eventstore.dbclient.*;
+import com.github.dockerjava.api.model.HealthCheck;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.nio.file.Paths;
+
+public class DockerContainerDatabase extends GenericContainer<DockerContainerDatabase> implements Database {
+    private static final String REGISTRY;
+    private static final HealthCheck HEALTH_CHECK;
+
+    static {
+        REGISTRY = "ghcr.io/eventstore";
+        HEALTH_CHECK = new HealthCheck()
+                .withInterval(1000000000L)
+                .withTimeout(1000000000L)
+                .withRetries(10);
+    }
+
+    public static class Builder {
+        String image;
+        String version;
+        boolean secure;
+        boolean anonymous;
+
+        public Builder() {
+            this.image = "eventstore";
+            this.version = "latest";
+        }
+
+        public Builder secure(boolean secure) {
+            this.secure = secure;
+            return this;
+        }
+
+        public Builder version(String version) {
+            this.version = version;
+            return this;
+        }
+
+        public Builder anonymous(boolean anonymous) {
+            this.anonymous = anonymous;
+            return this;
+        }
+
+        public Builder grpcTestDataImage() {
+            return this.image("testdata");
+        }
+
+        public Builder image(String image) {
+            this.image = image;
+            return this;
+        }
+
+        public DockerContainerDatabase build() {
+            return new DockerContainerDatabase(this);
+        }
+    }
+
+    private final Builder builder;
+    private final ClientTracker clientTracker;
+
+    public DockerContainerDatabase(Builder builder) {
+        super(String.format("%s/%s:%s", REGISTRY, builder.image, builder.version));
+        addExposedPorts(1113, 2113);
+
+        withEnv("EVENTSTORE_RUN_PROJECTIONS", "ALL");
+        withEnv("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true");
+
+        if (builder.secure) {
+            verifyCertificatesExist();
+            String certsDir = Paths.get(System.getProperty("user.dir"), "..", "certs").toAbsolutePath().toString();
+
+            withEnv("EVENTSTORE_CERTIFICATE_FILE", "/etc/eventstore/certs/node/node.crt");
+            withEnv("EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE", "/etc/eventstore/certs/node/node.key");
+            withEnv("EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH", "/etc/eventstore/certs/ca");
+            withFileSystemBind(certsDir, "/etc/eventstore/certs");
+        } else {
+            withEnv("EVENTSTORE_INSECURE", "true");
+        }
+
+        this.builder = builder;
+        this.clientTracker = new ClientTracker();
+
+        withCreateContainerCmdModifier(cmd -> cmd.withHealthcheck(HEALTH_CHECK));
+        waitingFor(Wait.forHealthcheck());
+
+        start();
+    }
+
+    @Override
+    public ConnectionSettingsBuilder defaultSettingsBuilder() {
+        ConnectionSettingsBuilder settingsBuilder = EventStoreDBClientSettings.builder().addHost(getHost(), getMappedPort(2113));
+
+        if (!builder.anonymous)
+            settingsBuilder.defaultCredentials("admin", "changeit");
+
+        if (builder.secure)
+            settingsBuilder.tls(true).tlsVerifyCert(false);
+        else
+            settingsBuilder.tls(false);
+
+        return settingsBuilder;
+    }
+
+    @Override
+    public ClientTracker getClientTracker() {
+        return clientTracker;
+    }
+
+    @Override
+    public void cleanup() {
+        try {
+            ExecResult result = execInContainer("tar", "-czvf", "/tmp/esdb_logs.tar.gz", "/var/log/eventstore");
+            if (result.getExitCode() != 0) {
+                logger().error(result.getStderr());
+                throw new RuntimeException("Error when compressing server logs");
+            }
+            copyFileFromContainer("/tmp/esdb_logs.tar.gz", "./esdb_logs.tar.gz");
+        } catch (Exception e) {
+            logger().error("Error when cleanup docker container", e);
+        } finally {
+            stop();
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private static void verifyCertificatesExist() {
+        String currentDir = System.getProperty("user.dir");
+        String[][] files =  {
+                 { "ca", "ca.crt" },
+                { "ca", "ca.key" },
+                { "node", "node.crt" },
+                { "node", "node.key" },
+        };
+
+        for (String[] strings : files) {
+            File file = Paths.get(currentDir, "..", "certs", strings[0], strings[1]).toAbsolutePath().toFile();
+
+            if (!file.exists())
+                throw new RuntimeException(new FileNotFoundException(file.getAbsolutePath()));
+        }
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/databases/ExternallyCreatedCluster.java b/db-client-java/src/test/java/com/eventstore/dbclient/databases/ExternallyCreatedCluster.java
new file mode 100644
index 0000000..cc081e1
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/databases/ExternallyCreatedCluster.java
@@ -0,0 +1,37 @@
+package com.eventstore.dbclient.databases;
+
+import com.eventstore.dbclient.*;
+
+public class ExternallyCreatedCluster implements Database {
+    final private boolean secure;
+    final private ClientTracker clientTracker;
+
+    public ExternallyCreatedCluster(boolean secure) {
+        this.secure = secure;
+        this.clientTracker = new ClientTracker();
+    }
+
+    @Override
+    public ConnectionSettingsBuilder defaultSettingsBuilder() {
+        return EventStoreDBClientSettings
+                .builder()
+                .defaultCredentials("admin", "changeit")
+                .addHost("localhost", 2_111)
+                .addHost("localhost", 2_112)
+                .addHost("localhost", 2_113)
+                .tls(secure)
+                .tlsVerifyCert(false)
+                .maxDiscoverAttempts(50)
+                .defaultDeadline(60_000);
+    }
+
+    @Override
+    public ClientTracker getClientTracker() {
+        return clientTracker;
+    }
+
+    @Override
+    public void cleanup() {
+        // Nothing do to here.
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/expectations/Expectations.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/Expectations.java
new file mode 100644
index 0000000..2814de7
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/Expectations.java
@@ -0,0 +1,19 @@
+package com.eventstore.dbclient.expectations;
+
+import com.eventstore.dbclient.*;
+
+import java.util.List;
+
+public interface Expectations extends ConnectionAware {
+    default void verifyAgainstTestData(List<ResolvedEvent> actualEvents, String filenameStem) {
+        ResolvedEvent[] actualEventsArray = actualEvents.toArray(new ResolvedEvent[0]);
+
+        TestResolvedEvent[] expectedEvents = TestDataLoader.loadSerializedResolvedEvents(filenameStem);
+        for (int i = 0; i < expectedEvents.length; i++) {
+            TestResolvedEvent expected = expectedEvents[i];
+            ResolvedEvent actual = actualEventsArray[i];
+
+            expected.assertEquals(actual);
+        }
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ProjectionManagementTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ProjectionManagementTests.java
similarity index 72%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ProjectionManagementTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/ProjectionManagementTests.java
index 8e826b7..55f956d 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ProjectionManagementTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ProjectionManagementTests.java
@@ -1,18 +1,13 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
 
 import java.io.BufferedReader;
-import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -20,45 +15,37 @@ import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-public class ProjectionManagementTests extends ESDBTests {
-    private static final String COUNT_EVENTS_PROJECTION_FILENAME = "count-events-projection.js";
-    private static final String COUNT_EVENTS_PARTITIONED_PROJECTION_FILENAME = "count-events-partitioned-projection.js";
-    private static final String UNKNOWN_KEYNAMES_PROJECTION_FILENAME = "state-with-unknown-keynames.js";
+public interface ProjectionManagementTests extends ConnectionAware {
+    String COUNT_EVENTS_PROJECTION_FILENAME = "count-events-projection.js";
+    String COUNT_EVENTS_PARTITIONED_PROJECTION_FILENAME = "count-events-partitioned-projection.js";
+    String UNKNOWN_KEYNAMES_PROJECTION_FILENAME = "state-with-unknown-keynames.js";
 
-    private static final int EXPECTED_EVENT_COUNT = 2000;
-    private final Logger logger = LoggerFactory.getLogger(ProjectionManagementTests.class);
+    int EXPECTED_EVENT_COUNT = 2000;
+    Logger logger = LoggerFactory.getLogger(ProjectionManagementTests.class);
 
-    private static String COUNT_EVENTS_PROJECTION;
-    private static String COUNT_EVENTS_PARTITIONED_PROJECTION;
-    private static String UNKNOWN_KEYNAMES_PROJECTION;
+    String COUNT_EVENTS_PROJECTION = loadResourceAsString(COUNT_EVENTS_PROJECTION_FILENAME);
+    String COUNT_EVENTS_PARTITIONED_PROJECTION = loadResourceAsString(COUNT_EVENTS_PARTITIONED_PROJECTION_FILENAME);
+    String UNKNOWN_KEYNAMES_PROJECTION = loadResourceAsString(UNKNOWN_KEYNAMES_PROJECTION_FILENAME);
 
-    private EventStoreDBProjectionManagementClient projectionClient;
-
-    @BeforeAll
-    public static void loadProjectionJs() throws IOException {
-
-        COUNT_EVENTS_PROJECTION = loadResourceAsString(COUNT_EVENTS_PROJECTION_FILENAME);
-        COUNT_EVENTS_PARTITIONED_PROJECTION = loadResourceAsString(COUNT_EVENTS_PARTITIONED_PROJECTION_FILENAME);
-        UNKNOWN_KEYNAMES_PROJECTION = loadResourceAsString(UNKNOWN_KEYNAMES_PROJECTION_FILENAME);
+    default EventStoreDBProjectionManagementClient projectionClient() {
+        return EventStoreDBProjectionManagementClient.from(getDefaultClient());
     }
 
-    private static String loadResourceAsString(String fileName) throws IOException {
+    static String loadResourceAsString(String fileName) {
 
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(ProjectionManagementTests.class.getClassLoader()
-                .getResourceAsStream(fileName)))) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(ProjectionManagementTests.class.getClassLoader()
+                .getResourceAsStream(fileName))))) {
 
             return reader.lines().collect(Collectors.joining("\n"));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
         }
     }
 
-    @BeforeEach
-    public void init() {
-        projectionClient = getEmptyServer().getProjectionClient();
-    }
-
     @Test
     @Order(1)
-    public void testCreateAndGetContinuousProjection() throws ExecutionException, InterruptedException {
+    default void testCreateAndGetContinuousProjection() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         String name = generateName();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
@@ -75,7 +62,8 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(2)
-    public void testDeserializingBasedOnJavaType() throws ExecutionException, InterruptedException {
+    default void testDeserializingBasedOnJavaType() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         String name = generateName();
         projectionClient
             .create(name, UNKNOWN_KEYNAMES_PROJECTION)
@@ -90,7 +78,8 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(3)
-    public void testEnablingProjection() throws ExecutionException, InterruptedException {
+    default void testEnablingProjection() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         String name = generateName();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
@@ -106,7 +95,8 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(4)
-    public void testDisablingProjection() throws ExecutionException, InterruptedException {
+    default void testDisablingProjection() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         String name = generateName();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
@@ -127,7 +117,8 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(5)
-    public void testAbortingProjection() throws ExecutionException, InterruptedException {
+    default void testAbortingProjection() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         String name = generateName();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
@@ -142,7 +133,8 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(6)
-    public void testResettingProjection() throws ExecutionException, InterruptedException {
+    default void testResettingProjection() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         String name = generateName();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
@@ -169,19 +161,16 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(7)
-    public void testDeletingProjection() throws ExecutionException, InterruptedException, TimeoutException {
+    default void testDeletingProjection() throws ExecutionException, InterruptedException, TimeoutException {
         String name = generateName();
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
             .get();
 
         waitUntilProjectionStatusIs(name, "Running");
 
-        if (EventStoreDB.isTestedAgainstVersion20()) {
-            projectionClient.abort(name).get();
-        } else {
-            projectionClient.disable(name).get();
-        }
+        projectionClient.disable(name).get();
 
         waitUntilProjectionStatusIs(name, "Stopped");
 
@@ -205,11 +194,14 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(8)
-    public void testGetProjectionStatistics() throws ExecutionException, InterruptedException {
+    default void testGetProjectionStatistics() throws Exception {
+        Exceptions exceptions = new Exceptions().registerUnknownError();
         String name = generateName();
-        projectionClient
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
+
+        flaky(10, exceptions, () -> projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
-            .get();
+            .get());
 
         waitUntilProjectionStatusIs(name, "Running");
 
@@ -226,8 +218,9 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(9)
-    public void testUpdateProjection() throws ExecutionException, InterruptedException {
+    default void testUpdateProjection() throws ExecutionException, InterruptedException {
         String name = generateName();
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
             .get();
@@ -245,8 +238,9 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(10)
-    public void testGetProjectionState() throws ExecutionException, InterruptedException {
+    default void testGetProjectionState() throws ExecutionException, InterruptedException {
         String name = generateName();
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
             .get();
@@ -262,8 +256,9 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(11)
-    public void testGetProjectionResultByPartition() throws ExecutionException, InterruptedException {
+    default void testGetProjectionResultByPartition() throws ExecutionException, InterruptedException {
         String name = generateName();
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient
             .create(name, COUNT_EVENTS_PARTITIONED_PROJECTION)
             .get();
@@ -300,8 +295,9 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(12)
-    public void testGetProjectionStateByPartition() throws ExecutionException, InterruptedException {
+    default void testGetProjectionStateByPartition() throws ExecutionException, InterruptedException {
         String name = generateName();
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient
             .create(name, COUNT_EVENTS_PARTITIONED_PROJECTION)
             .get();
@@ -335,8 +331,9 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(13)
-    public void testGetProjectionStatus() throws ExecutionException, InterruptedException {
+    default void testGetProjectionStatus() throws ExecutionException, InterruptedException {
         String name = generateName();
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient
             .create(name, COUNT_EVENTS_PROJECTION)
             .get();
@@ -358,13 +355,13 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(14)
-    public void testListProjections() throws ExecutionException, InterruptedException {
-
+    default void testListProjections() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         List<ProjectionDetails> projections = projectionClient.list().get();
         Assertions.assertTrue(projections.size() > 1);
     }
 
-    private void assertDeserializedIntoMap(final Map<String, Item> result) {
+    default void assertDeserializedIntoMap(final Map<String, Item> result) {
 
         Assertions.assertNotNull(result);
         Assertions.assertFalse(result.entrySet().isEmpty());
@@ -377,18 +374,20 @@ public class ProjectionManagementTests extends ESDBTests {
 
     @Test
     @Order(15)
-    public void testRestartingProjectionSubsystem() throws ExecutionException, InterruptedException {
+    default void testRestartingProjectionSubsystem() throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         projectionClient.restartSubsystem().get();
     }
 
-    private Map<String, Item> getResultOfUnknownKeyNamesProjection(String name) throws ExecutionException, InterruptedException {
+    default Map<String, Item> getResultOfUnknownKeyNamesProjection(String name) throws ExecutionException, InterruptedException {
+        EventStoreDBProjectionManagementClient projectionClient = projectionClient();
 
         return projectionClient
             .<Map<String, Item>>getResult(name, factory -> factory.constructMapType(HashMap.class, String.class, Item.class))
             .get();
     }
 
-    private void assertCountingProjectionResultAsExpected(final CountResult result) {
+    default void assertCountingProjectionResultAsExpected(final CountResult result) {
 
         Assertions.assertNotNull(result);
         //The projection may not have completed so may not yet equal EXPECTED_EVENT_COUNT
@@ -424,7 +423,8 @@ public class ProjectionManagementTests extends ESDBTests {
         }
     }
 
-    private void waitUntilProjectionStatusIs(String name, String... statuses) throws ExecutionException, InterruptedException {
+    default void waitUntilProjectionStatusIs(String name, String... statuses) throws ExecutionException, InterruptedException {
+        final EventStoreDBProjectionManagementClient projectionClient = projectionClient();
         CompletableFuture.runAsync(() -> {
             String last = "";
             for (int i = 0; i < 6; i++) {
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ReadAllReactiveTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadAllReactiveTests.java
similarity index 59%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ReadAllReactiveTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadAllReactiveTests.java
index e144886..23b6cc2 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ReadAllReactiveTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadAllReactiveTests.java
@@ -1,18 +1,18 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import io.reactivex.rxjava3.core.Flowable;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 
 import static java.util.stream.Collectors.toList;
 
-public class ReadAllReactiveTests extends ESDBTests {
+public interface ReadAllReactiveTests extends Expectations {
     @Test
-    public void testReadAllEventsReactiveForwardFromZeroPosition() throws ExecutionException, InterruptedException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadAllEventsReactiveForwardFromZeroPosition() throws ExecutionException, InterruptedException {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadAllOptions options = ReadAllOptions.get()
                 .forwards()
@@ -30,8 +30,8 @@ public class ReadAllReactiveTests extends ESDBTests {
     }
 
     @Test
-    public void testReadAllEventsReactiveForwardFromNonZeroPosition() throws ExecutionException, InterruptedException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadAllEventsReactiveForwardFromNonZeroPosition() throws ExecutionException, InterruptedException {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadAllOptions options = ReadAllOptions.get()
                 .forwards()
@@ -49,8 +49,8 @@ public class ReadAllReactiveTests extends ESDBTests {
     }
 
     @Test
-    public void testReadAllEventsReactiveBackwardsFromNonZeroPosition() throws ExecutionException, InterruptedException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadAllEventsReactiveBackwardsFromNonZeroPosition() throws ExecutionException, InterruptedException {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadAllOptions options = ReadAllOptions.get()
                 .backwards()
@@ -66,16 +66,4 @@ public class ReadAllReactiveTests extends ESDBTests {
 
         verifyAgainstTestData(events, "all-back-c3386-p3386");
     }
-
-    private void verifyAgainstTestData(List<ResolvedEvent> actualEvents, String filenameStem) {
-        ResolvedEvent[] actualEventsArray = actualEvents.toArray(new ResolvedEvent[0]);
-
-        TestResolvedEvent[] expectedEvents = TestDataLoader.loadSerializedResolvedEvents(filenameStem);
-        for (int i = 0; i < expectedEvents.length; i++) {
-            TestResolvedEvent expected = expectedEvents[i];
-            ResolvedEvent actual = actualEventsArray[i];
-
-            expected.assertEquals(actual);
-        }
-    }
 }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ReadAllTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadAllTests.java
similarity index 50%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ReadAllTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadAllTests.java
index be30eb6..c54f4cf 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ReadAllTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadAllTests.java
@@ -1,15 +1,15 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 
-public class ReadAllTests extends ESDBTests {
+public interface ReadAllTests extends Expectations {
     @Test
-    public void testReadAllEventsForwardFromZeroPosition() throws ExecutionException, InterruptedException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadAllEventsForwardFromZeroPosition() throws ExecutionException, InterruptedException {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadAllOptions options = ReadAllOptions.get()
                 .forwards()
@@ -24,8 +24,8 @@ public class ReadAllTests extends ESDBTests {
     }
 
     @Test
-    public void testReadAllEventsForwardFromNonZeroPosition() throws ExecutionException, InterruptedException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadAllEventsForwardFromNonZeroPosition() throws ExecutionException, InterruptedException {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadAllOptions options = ReadAllOptions.get()
                 .forwards()
@@ -40,8 +40,8 @@ public class ReadAllTests extends ESDBTests {
     }
 
     @Test
-    public void testReadAllEventsBackwardsFromNonZeroPosition() throws ExecutionException, InterruptedException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadAllEventsBackwardsFromNonZeroPosition() throws ExecutionException, InterruptedException {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadAllOptions options = ReadAllOptions.get()
                 .backwards()
@@ -54,16 +54,4 @@ public class ReadAllTests extends ESDBTests {
 
         verifyAgainstTestData(result.getEvents(), "all-back-c3386-p3386");
     }
-
-    private void verifyAgainstTestData(List<ResolvedEvent> actualEvents, String filenameStem) {
-        ResolvedEvent[] actualEventsArray = actualEvents.toArray(new ResolvedEvent[0]);
-
-        TestResolvedEvent[] expectedEvents = TestDataLoader.loadSerializedResolvedEvents(filenameStem);
-        for (int i = 0; i < expectedEvents.length; i++) {
-            TestResolvedEvent expected = expectedEvents[i];
-            ResolvedEvent actual = actualEventsArray[i];
-
-            expected.assertEquals(actual);
-        }
-    }
 }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ReadStreamReactiveTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadStreamReactiveTests.java
similarity index 57%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ReadStreamReactiveTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadStreamReactiveTests.java
index 062b477..93efea1 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ReadStreamReactiveTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadStreamReactiveTests.java
@@ -1,19 +1,21 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import io.reactivex.rxjava3.core.Flowable;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.List;
-import java.util.Optional;
 
 import static java.util.stream.Collectors.toList;
 
-public class ReadStreamReactiveTests extends ESDBTests {
+public interface ReadStreamReactiveTests extends Expectations {
     @Test
-    public void testReadStreamReactiveForward10EventsFromPositionStart() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadStreamReactiveForward10EventsFromPositionStart() throws Throwable {
+        Assumptions.assumeFalse(getDatabase().isTargetingBelowOrEqual21_10());
+
+        EventStoreDBClient client = getDefaultClient();
 
         ReadStreamOptions options = ReadStreamOptions.get()
                 .forwards()
@@ -29,13 +31,14 @@ public class ReadStreamReactiveTests extends ESDBTests {
 
         verifyAgainstTestData(
                 events,
-                "dataset20M-1800-e0-e10",
-                client.getGrpcClient().getServerVersion().get());
+                "dataset20M-1800-e0-e10");
     }
 
     @Test
-    public void testReadStreamReactiveBackward10EventsFromPositionEnd() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadStreamReactiveBackward10EventsFromPositionEnd() throws Throwable {
+        Assumptions.assumeFalse(getDatabase().isTargetingBelowOrEqual21_10());
+
+        EventStoreDBClient client = getDefaultClient();
 
         ReadStreamOptions options = ReadStreamOptions.get()
                 .backwards()
@@ -51,13 +54,12 @@ public class ReadStreamReactiveTests extends ESDBTests {
 
         verifyAgainstTestData(
                 events,
-                "dataset20M-1800-e1999-e1990",
-                client.getGrpcClient().getServerVersion().get());
+                "dataset20M-1800-e1999-e1990");
     }
 
     @Test
-    public void testReadStreamOptionsAreNotIgnoredInOverloadedMethod() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadStreamReactiveOptionsAreNotIgnoredInOverloadedMethod() throws Throwable {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadStreamOptions options = ReadStreamOptions.get()
                 .backwards()
@@ -82,23 +84,4 @@ public class ReadStreamReactiveTests extends ESDBTests {
         Assertions.assertEquals(events1.size(), events2.size());
         Assertions.assertEquals(firstEvent1.getEventId(), firstEvent2.getEventId());
     }
-
-    private void verifyAgainstTestData(List<ResolvedEvent> actualEvents, String filenameStem, Optional<ServerVersion> serverVersion) {
-        ResolvedEvent[] actualEventsArray = actualEvents.toArray(new ResolvedEvent[0]);
-
-        TestResolvedEvent[] expectedEvents = TestDataLoader.loadSerializedResolvedEvents(filenameStem);
-        for (int i = 0; i < expectedEvents.length; i++) {
-            TestResolvedEvent expected = expectedEvents[i];
-            ResolvedEvent actual = actualEventsArray[i];
-
-            if (!serverVersion.isPresent() || serverVersion.get().isLessThan(22, 6, 0)) {
-                TestPosition notSet = new TestPosition();
-                notSet.setPrepare(-1);
-                notSet.setCommit(-1);
-                expected.getEvent().setPosition(notSet);
-            }
-
-            expected.assertEquals(actual);
-        }
-    }
 }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ReadStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadStreamTests.java
similarity index 51%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ReadStreamTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadStreamTests.java
index bb5a340..3e5a709 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ReadStreamTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/ReadStreamTests.java
@@ -1,18 +1,17 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import io.reactivex.rxjava3.core.Flowable;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
 
-import java.util.List;
-import java.util.Optional;
-
-public class ReadStreamTests extends ESDBTests {
+public interface ReadStreamTests extends Expectations {
     @Test
-    public void testReadStreamForward10EventsFromPositionStart() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadStreamForward10EventsFromPositionStart() throws Throwable {
+        Assumptions.assumeFalse(getDatabase().isTargetingBelowOrEqual21_10());
+
+        EventStoreDBClient client = getDefaultClient();
 
         ReadStreamOptions options = ReadStreamOptions.get()
                 .forwards()
@@ -25,13 +24,14 @@ public class ReadStreamTests extends ESDBTests {
 
         verifyAgainstTestData(
                 result.getEvents(),
-                "dataset20M-1800-e0-e10",
-                client.getGrpcClient().getServerVersion().get());
+                "dataset20M-1800-e0-e10");
     }
 
     @Test
-    public void testReadStreamBackward10EventsFromPositionEnd() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadStreamBackward10EventsFromPositionEnd() throws Throwable {
+        Assumptions.assumeFalse(getDatabase().isTargetingBelowOrEqual21_10());
+
+        EventStoreDBClient client = getDefaultClient();
         ReadStreamOptions options = ReadStreamOptions.get()
                 .backwards()
                 .fromEnd()
@@ -43,13 +43,12 @@ public class ReadStreamTests extends ESDBTests {
 
         verifyAgainstTestData(
                 result.getEvents(),
-                "dataset20M-1800-e1999-e1990",
-                client.getGrpcClient().getServerVersion().get());
+                "dataset20M-1800-e1999-e1990");
     }
 
     @Test
-    public void testReadStreamOptionsAreNotIgnoredInOverloadedMethod() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testReadStreamOptionsAreNotIgnoredInOverloadedMethod() throws Throwable {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadStreamOptions options = ReadStreamOptions.get()
                 .backwards()
@@ -70,12 +69,8 @@ public class ReadStreamTests extends ESDBTests {
     }
 
     @Test
-    public void testStreamPositionWhenReading() throws Throwable {
-        if (EventStoreDB.isTestedAgains20_10()) {
-            return;
-        }
-
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testStreamPositionWhenReading() throws Throwable {
+        EventStoreDBClient client = getDefaultClient();
 
         ReadStreamOptions options = ReadStreamOptions.get()
                 .forwards()
@@ -89,24 +84,4 @@ public class ReadStreamTests extends ESDBTests {
 
         Assertions.assertEquals(1_999, last);
     }
-
-
-    private void verifyAgainstTestData(List<ResolvedEvent> actualEvents, String filenameStem, Optional<ServerVersion> serverVersion) {
-        ResolvedEvent[] actualEventsArray = actualEvents.toArray(new ResolvedEvent[0]);
-
-        TestResolvedEvent[] expectedEvents = TestDataLoader.loadSerializedResolvedEvents(filenameStem);
-        for (int i = 0; i < expectedEvents.length; i++) {
-            TestResolvedEvent expected = expectedEvents[i];
-            ResolvedEvent actual = actualEventsArray[i];
-
-            if (!serverVersion.isPresent() || serverVersion.get().isLessThan(22, 6, 0)) {
-                TestPosition notSet = new TestPosition();
-                notSet.setPrepare(-1);
-                notSet.setCommit(-1);
-                expected.getEvent().setPosition(notSet);
-            }
-
-            expected.assertEquals(actual);
-        }
-    }
 }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/SubscribeToAllTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/SubscribeToAllTests.java
similarity index 86%
rename from db-client-java/src/test/java/com/eventstore/dbclient/SubscribeToAllTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/SubscribeToAllTests.java
index 1b1b941..72af78d 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/SubscribeToAllTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/SubscribeToAllTests.java
@@ -1,23 +1,24 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.fail;
 
-public class SubscribeToAllTests extends ESDBTests {
+public interface SubscribeToAllTests extends Expectations {
 
     @Test
-    public void testStreamSubscriptionIsAbortedByShutdownOfClient() throws InterruptedException, ExecutionException {
-        EventStoreDBClient streams = EventStoreDBClient.create(getPopulatedServer().getSettings());
+    default void testStreamSubscriptionToAllIsAbortedByShutdownOfClient() throws InterruptedException, ExecutionException {
+        EventStoreDBClient streams = getDatabase().newClient();
 
         final CountDownLatch aborted = new CountDownLatch(1);
 
@@ -53,8 +54,8 @@ public class SubscribeToAllTests extends ESDBTests {
 
 
     @Test
-    public void testAllSubscriptionDeliversAllowsCancellationDuringStream() throws InterruptedException, ExecutionException {
-        EventStoreDBClient streams = getPopulatedServer().getClient();
+    default void testAllSubscriptionDeliversAllowsCancellationDuringStream() throws InterruptedException, ExecutionException {
+        EventStoreDBClient streams = getDefaultClient();
 
         final CountDownLatch receivedEvents = new CountDownLatch(1000);
         final CountDownLatch cancellation = new CountDownLatch(1);
@@ -88,7 +89,7 @@ public class SubscribeToAllTests extends ESDBTests {
     }
 
     @Test
-    public void testAllSubscriptionWithFilterDeliversCorrectEvents() throws InterruptedException, ExecutionException {
+    default void testAllSubscriptionWithFilterDeliversCorrectEvents() throws InterruptedException, ExecutionException {
         final TestPosition[] expectedPositions = TestDataLoader.loadSerializedPositions(
                 "all-positions-filtered-stream194-e0-e30");
         final long[] expectedStreamVersions = TestDataLoader.loadSerializedStreamVersions(
@@ -132,7 +133,7 @@ public class SubscribeToAllTests extends ESDBTests {
                 .fromStart()
                 .filter(filter);
 
-        Subscription result = getPopulatedServer().getClient()
+        Subscription result = getDefaultClient()
                 .subscribeToAll(listener, options)
                 .get();
 
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/SubscribeToStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/SubscribeToStreamTests.java
similarity index 87%
rename from db-client-java/src/test/java/com/eventstore/dbclient/SubscribeToStreamTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/SubscribeToStreamTests.java
index 28fd581..c99e6a1 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/SubscribeToStreamTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/SubscribeToStreamTests.java
@@ -1,21 +1,22 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.*;
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 
 import static org.junit.jupiter.api.Assertions.*;
 
-public class SubscribeToStreamTests extends ESDBTests {
+public interface SubscribeToStreamTests extends Expectations {
 
     @Test
-    public void testStreamSubscriptionIsAbortedByShutdownOfClient() throws InterruptedException, ExecutionException {
-        EventStoreDBClient client = EventStoreDBClient.create(getPopulatedServer().getSettings());
+    default void testStreamSubscriptionIsAbortedByShutdownOfClient() throws InterruptedException, ExecutionException {
+        EventStoreDBClient client = getDatabase().newClient();
 
         final CountDownLatch aborted = new CountDownLatch(1);
 
@@ -46,8 +47,8 @@ public class SubscribeToStreamTests extends ESDBTests {
     }
 
     @Test
-    public void testStreamSubscriptionDeliversAllowsCancellationDuringStream() throws InterruptedException, ExecutionException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testStreamSubscriptionDeliversAllowsCancellationDuringStream() throws InterruptedException, ExecutionException {
+        EventStoreDBClient client = getDefaultClient();
 
         final CountDownLatch receivedEvents = new CountDownLatch(1000);
         final CountDownLatch cancellation = new CountDownLatch(1);
@@ -78,8 +79,8 @@ public class SubscribeToStreamTests extends ESDBTests {
     }
 
     @Test
-    public void testStreamSubscriptionDeliversAllEventsInStream() throws InterruptedException, ExecutionException {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testStreamSubscriptionDeliversAllEventsInStream() throws InterruptedException, ExecutionException {
+        EventStoreDBClient client = getDefaultClient();
 
         final CountDownLatch receivedEvents = new CountDownLatch(6000);
         final CountDownLatch cancellation = new CountDownLatch(1);
@@ -116,8 +117,8 @@ public class SubscribeToStreamTests extends ESDBTests {
     }
 
     @Test
-    public void testStreamSubscriptionDeliversAllEventsInStreamAndListensForNewEvents() throws Throwable {
-        EventStoreDBClient client = getPopulatedServer().getClient();
+    default void testStreamSubscriptionDeliversAllEventsInStreamAndListensForNewEvents() throws Throwable {
+        EventStoreDBClient client = getDefaultClient();
 
         final CountDownLatch receivedEvents = new CountDownLatch(6000);
         final CountDownLatch appendedEvents = new CountDownLatch(1);
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/TestDataLoader.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/TestDataLoader.java
similarity index 97%
rename from db-client-java/src/test/java/com/eventstore/dbclient/TestDataLoader.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/TestDataLoader.java
index 69cb3ff..ed916d4 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/TestDataLoader.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/TestDataLoader.java
@@ -1,4 +1,4 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/TestRecordedEvent.java b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/TestRecordedEvent.java
similarity index 96%
rename from db-client-java/src/test/java/com/eventstore/dbclient/TestRecordedEvent.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/expectations/TestRecordedEvent.java
index bf219f8..8ad756c 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/TestRecordedEvent.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/expectations/TestRecordedEvent.java
@@ -1,5 +1,8 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.expectations;
 
+import com.eventstore.dbclient.Position;
+import com.eventstore.dbclient.RecordedEvent;
+import com.eventstore.dbclient.ResolvedEvent;
 import org.junit.jupiter.api.Assertions;
 
 import java.time.Instant;
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/EventDataTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/EventDataTests.java
similarity index 83%
rename from db-client-java/src/test/java/com/eventstore/dbclient/EventDataTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/EventDataTests.java
index ec5121c..6e0adc3 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/EventDataTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/EventDataTests.java
@@ -1,5 +1,7 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
+import com.eventstore.dbclient.EventData;
+import com.eventstore.dbclient.EventDataBuilder;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ExpectedRevisionTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ExpectedRevisionTests.java
similarity index 97%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ExpectedRevisionTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/ExpectedRevisionTests.java
index bd5a93a..8c3c8ed 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ExpectedRevisionTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ExpectedRevisionTests.java
@@ -1,6 +1,7 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
 
+import com.eventstore.dbclient.ExpectedRevision;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/NodeSelectorTest.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/NodeSelectorTest.java
similarity index 82%
rename from db-client-java/src/test/java/com/eventstore/dbclient/NodeSelectorTest.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/NodeSelectorTest.java
index 310e814..4b4b63a 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/NodeSelectorTest.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/NodeSelectorTest.java
@@ -1,5 +1,9 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
+import com.eventstore.dbclient.ClusterInfo;
+import com.eventstore.dbclient.NodePreference;
+import com.eventstore.dbclient.NodeSelector;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -11,7 +15,6 @@ import java.util.stream.IntStream;
 import static java.util.Arrays.asList;
 import static java.util.UUID.randomUUID;
 import static java.util.stream.Collectors.toSet;
-import static org.junit.Assert.*;
 
 public class NodeSelectorTest {
 
@@ -44,7 +47,7 @@ public class NodeSelectorTest {
 
         Optional<ClusterInfo.Member> selectedNode = sut.determineBestFitNode(clusterInfo);
 
-        assertFalse(selectedNode.isPresent());
+        Assertions.assertFalse(selectedNode.isPresent());
     }
 
     @Test
@@ -57,7 +60,7 @@ public class NodeSelectorTest {
 
         Optional<ClusterInfo.Member> selectedNode = sut.determineBestFitNode(clusterInfo);
 
-        assertFalse(selectedNode.isPresent());
+        Assertions.assertFalse(selectedNode.isPresent());
     }
 
     @Test
@@ -66,8 +69,8 @@ public class NodeSelectorTest {
 
         Set<UUID> members = performMultipleNodeSelectionsWithSelector(sut);
 
-        assertEquals(1, members.size());
-        assertTrue(members.contains(leader.getInstanceId()));
+        Assertions.assertEquals(1, members.size());
+        Assertions.assertTrue(members.contains(leader.getInstanceId()));
     }
 
     @Test
@@ -76,8 +79,8 @@ public class NodeSelectorTest {
 
         Set<UUID> members = performMultipleNodeSelectionsWithSelector(sut);
 
-        assertEquals(2, members.size());
-        assertTrue(members.containsAll(asList(follower1.getInstanceId(), follower2.getInstanceId())));
+        Assertions.assertEquals(2, members.size());
+        Assertions.assertTrue(members.containsAll(asList(follower1.getInstanceId(), follower2.getInstanceId())));
     }
 
     @Test
@@ -86,8 +89,8 @@ public class NodeSelectorTest {
 
         Set<UUID> members = performMultipleNodeSelectionsWithSelector(sut);
 
-        assertEquals(2, members.size());
-        assertTrue(members.containsAll(asList(replica1.getInstanceId(), replica2.getInstanceId())));
+        Assertions.assertEquals(2, members.size());
+        Assertions.assertTrue(members.containsAll(asList(replica1.getInstanceId(), replica2.getInstanceId())));
     }
 
     @Test
@@ -96,7 +99,7 @@ public class NodeSelectorTest {
 
         Set<UUID> members = performMultipleNodeSelectionsWithSelector(sut);
 
-        assertEquals(5, members.size());
+        Assertions.assertEquals(5, members.size());
     }
 
     private Set<UUID> performMultipleNodeSelectionsWithSelector(NodeSelector sut) {
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/OfflineMetadataTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java
similarity index 91%
rename from db-client-java/src/test/java/com/eventstore/dbclient/OfflineMetadataTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java
index 3dbdc11..c6ff7b9 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/OfflineMetadataTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java
@@ -1,5 +1,8 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
+import com.eventstore.dbclient.Acl;
+import com.eventstore.dbclient.Acls;
+import com.eventstore.dbclient.StreamMetadata;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ParseInvalidConnectionStringTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseInvalidConnectionStringTests.java
similarity index 90%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ParseInvalidConnectionStringTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseInvalidConnectionStringTests.java
index 4a5e038..48c82e2 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ParseInvalidConnectionStringTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseInvalidConnectionStringTests.java
@@ -1,5 +1,8 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
+import com.eventstore.dbclient.ConnectionStringParsingException;
+import com.eventstore.dbclient.EventStoreDBClientSettings;
+import com.eventstore.dbclient.EventStoreDBConnectionString;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ParseValidConnectionStringTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java
similarity index 98%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ParseValidConnectionStringTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java
index 1d134a1..e76c007 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ParseValidConnectionStringTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java
@@ -1,9 +1,13 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
 import java.net.InetSocketAddress;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
+import com.eventstore.dbclient.ConnectionSettingsBuilder;
+import com.eventstore.dbclient.ConnectionStringParsingException;
+import com.eventstore.dbclient.EventStoreDBClientSettings;
+import com.eventstore.dbclient.EventStoreDBConnectionString;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.json.JsonMapper;
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/PositionTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/PositionTests.java
similarity index 96%
rename from db-client-java/src/test/java/com/eventstore/dbclient/PositionTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/PositionTests.java
index 03a04c7..f03fa32 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/PositionTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/PositionTests.java
@@ -1,5 +1,6 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
+import com.eventstore.dbclient.Position;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ServerVersionTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ServerVersionTests.java
similarity index 95%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ServerVersionTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/misc/ServerVersionTests.java
index 238cb87..9e817d6 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ServerVersionTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ServerVersionTests.java
@@ -1,5 +1,6 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.misc;
 
+import com.eventstore.dbclient.ServerVersion;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/CreatePersistentSubscriptionTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/CreatePersistentSubscriptionTests.java
new file mode 100644
index 0000000..aeea971
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/CreatePersistentSubscriptionTests.java
@@ -0,0 +1,28 @@
+package com.eventstore.dbclient.persistentsubscriptions;
+
+import com.eventstore.dbclient.*;
+import org.junit.jupiter.api.Test;
+
+public interface CreatePersistentSubscriptionTests extends ConnectionAware {
+    @Test
+    default void testCreatePersistentSub() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+
+        client.createToStream(generateName(), generateName())
+                .get();
+
+        client.createToStream(generateName(), generateName(), CreatePersistentSubscriptionToStreamOptions.get().startFrom(2))
+                .get();
+    }
+
+    @Test
+    default void testCreatePersistentSubToAll() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+
+        client.createToAll(generateName())
+                .get();
+
+        client.createToAll(generateName(), CreatePersistentSubscriptionToAllOptions.get().startFrom(1, 2))
+                .get();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/DeletePersistentSubscriptionToStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/DeletePersistentSubscriptionToStreamTests.java
new file mode 100644
index 0000000..4f1938b
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/DeletePersistentSubscriptionToStreamTests.java
@@ -0,0 +1,35 @@
+package com.eventstore.dbclient.persistentsubscriptions;
+
+import com.eventstore.dbclient.ConnectionAware;
+import com.eventstore.dbclient.EventStoreDBPersistentSubscriptionsClient;
+import com.eventstore.dbclient.UnsupportedFeatureException;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.ExecutionException;
+
+public interface DeletePersistentSubscriptionToStreamTests extends ConnectionAware {
+    @Test
+    default void testDeletePersistentSub() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String streamName = generateName();
+        String groupName = generateName();
+
+        client.createToStream(streamName, groupName)
+                .get();
+
+        client.deleteToStream(streamName, groupName)
+                .get();
+    }
+
+    @Test
+    default void testDeletePersistentSubToAll() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String groupName = generateName();
+
+        client.createToAll(groupName)
+                .get();
+
+        client.deleteToAll(groupName)
+                .get();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/PersistentSubscriptionManagementTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/PersistentSubscriptionManagementTests.java
new file mode 100644
index 0000000..413a6c9
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/PersistentSubscriptionManagementTests.java
@@ -0,0 +1,282 @@
+package com.eventstore.dbclient.persistentsubscriptions;
+
+import com.eventstore.dbclient.*;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.junit.jupiter.api.*;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@SuppressWarnings("unchecked")
+public interface PersistentSubscriptionManagementTests extends ConnectionAware {
+    @Test
+    @Order(1)
+    default void testListPersistentSubscriptions() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String groupName = generateName();
+        String streamA = generateName();
+        String streamB = generateName();
+
+        client.createToStream(streamA, groupName)
+                .get();
+
+        client.createToStream(streamB, groupName)
+                .get();
+
+        List<PersistentSubscriptionInfo> subs = client.listAll().get();
+
+        int count = 0;
+        for (PersistentSubscriptionInfo info: subs) {
+            if (info.getEventSource().equals(streamA) || info.getEventSource().equals(streamB)) {
+                count++;
+            }
+        }
+
+        Assertions.assertEquals(2, count);
+    }
+
+    @Test
+    @Order(2)
+    default void testListPersistentSubscriptionsForStream() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
+
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String streamName = generateName();
+        String groupName = generateName();
+
+        flaky(10, exceptions, () -> client.createToStream(streamName, groupName).get());
+
+        exceptions.register(ResourceNotFoundException.class);
+
+        List<PersistentSubscriptionToStreamInfo> subs = flaky(10, exceptions, () -> client.listToStream(streamName).get());
+
+        Assertions.assertEquals(subs.size(), 1);
+        Assertions.assertEquals(subs.get(0).getEventSource(), streamName);
+    }
+
+    @Test
+    @Order(3)
+    default void testListPersistentSubscriptionsToAll() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String groupName = generateName();
+        client.createToAll(groupName)
+                .get();
+
+        List<PersistentSubscriptionToAllInfo> subs = client.listToAll().get();
+
+        Assertions.assertTrue(subs.size() >= 1);
+
+        boolean found = false;
+
+        for (PersistentSubscriptionToAllInfo info : subs) {
+            if (info.getEventSource().equals("$all") && info.getGroupName().equals(groupName)) {
+                found = true;
+                break;
+            }
+        }
+
+        Assertions.assertTrue(found);
+    }
+
+    @Test
+    @Order(4)
+    default void testGetPersistentSubscriptionInfo() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String streamName = generateName();
+        String groupName = generateName();
+        client.createToStream(streamName, groupName)
+                .get();
+        Optional<PersistentSubscriptionToStreamInfo> result = Optional.empty();
+        for (int i = 0; i < 10; i++) {
+            result = client.getInfoToStream(streamName, groupName).get();
+            if (!result.isPresent()) {
+                Thread.sleep(1000);
+                continue;
+            }
+
+            break;
+        }
+
+        Assertions.assertTrue(result.isPresent());
+        Assertions.assertEquals(streamName, result.get().getEventSource());
+    }
+
+    @Test
+    @Order(5)
+    default void testGetPersistentSubscriptionInfoToAll() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String groupName = generateName();
+
+        client.createToAll(groupName)
+                .get();
+
+        Optional<PersistentSubscriptionToAllInfo> result = client.getInfoToAll(groupName).get();
+
+        Assertions.assertTrue(result.isPresent());
+        Assertions.assertEquals("$all", result.get().getEventSource());
+        Assertions.assertEquals(result.get().getGroupName(), groupName);
+    }
+
+    @Test
+    @Order(6)
+     default void testGetPersistentSubscriptionInfoNotExisting() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        Optional<PersistentSubscriptionToStreamInfo> result = client.getInfoToStream(generateName(), generateName()).get();
+
+        Assertions.assertFalse(result.isPresent());
+    }
+
+    @Test
+    @Order(7)
+    @Timeout(value = 2, unit = TimeUnit.MINUTES)
+    default void testReplayParkedMessages() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        final EventStoreDBClient streamClient = getDatabase().defaultClient();
+        final String streamName = generateName();
+        final String groupName = generateName();
+
+        flaky(10, exceptions, () -> client.createToStream(streamName, groupName).get());
+
+        final CompletableFuture allNacked = new CompletableFuture();
+        final CompletableFuture allReplayed = new CompletableFuture();
+
+        client.subscribeToStream(streamName, groupName, new PersistentSubscriptionListener() {
+            int count = 0;
+            @Override
+            public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) {
+                if (count < 2)
+                    subscription.nack(NackAction.Park, "because reason", event);
+                else
+                    subscription.ack(event);
+
+                count++;
+
+                if (count == 2) {
+                    allNacked.complete(null);
+                }
+
+                if (count >= 4) {
+                    allReplayed.complete(null);
+                    subscription.stop();
+                }
+            }
+        }).get();
+
+        for (int i = 0; i < 2; i++) {
+            EventData data = EventData.builderAsJson("foobar", new Foo()).build();
+            streamClient.appendToStream(streamName, data).get();
+        }
+
+        exceptions.register(ResourceNotFoundException.class);
+
+        CompletableFuture.runAsync(() -> {
+            try {
+                allNacked.get();
+                // We give the server some time to park those events.
+                Thread.sleep(10_000);
+                flaky(10, exceptions, () -> client.replayParkedMessagesToStream(streamName, groupName).get());
+                allReplayed.get();
+            } catch (Exception e) {
+                getLogger().error("Error when running testReplayParkedMessages", e);
+                throw new RuntimeException(e);
+            }
+        }).get();
+    }
+
+    @Test
+    @Order(8)
+    @Timeout(value = 2, unit = TimeUnit.MINUTES)
+    default void testReplayParkedMessagesToAll() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        final EventStoreDBClient streamClient = getDatabase().defaultClient();
+        String streamName = generateName();
+        String groupName = generateName();
+
+        flaky(10, exceptions, () -> client.createToAll(groupName).get());
+
+        final CompletableFuture allNacked = new CompletableFuture();
+        final CompletableFuture allReplayed = new CompletableFuture();
+
+        client.subscribeToAll(groupName, new PersistentSubscriptionListener() {
+            int count = 0;
+            @Override
+            public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) {
+                if (count < 2 && event.getOriginalEvent().getStreamId().equals(streamName))
+                    subscription.nack(NackAction.Park, "because reason", event);
+                else
+                    subscription.ack(event);
+
+                if (event.getOriginalEvent().getStreamId().equals(streamName) || event.getEvent().getStreamId().equals(streamName))
+                    count++;
+
+                if (count == 2) {
+                    allNacked.complete(null);
+                }
+
+                if (count >= 4) {
+                    allReplayed.complete(null);
+                    subscription.stop();
+                }
+            }
+        }).get();
+
+        for (int i = 0; i < 2; i++) {
+            EventData data = EventData.builderAsJson("foobar", new Foo()).build();
+            streamClient.appendToStream(streamName, data).get();
+        }
+
+        exceptions.register(ResourceNotFoundException.class);
+        CompletableFuture.runAsync(() -> {
+            try {
+                allNacked.get();
+                // We give the server some time to park those events.
+                Thread.sleep(10_000);
+                flaky(10, exceptions, () -> client.replayParkedMessagesToAll(groupName).get());
+                allReplayed.get();
+            } catch (Exception e) {
+                getLogger().error("Error when running testReplayParkedMessages", e);
+                throw new RuntimeException(e);
+            }
+        }).get();
+    }
+
+    @Test
+    @Order(9)
+    default void testEncoding() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        String streamName = String.format("/foo/%s/stream", generateName());
+        String groupName = String.format("/foo/%s/group", generateName());
+
+        flaky(10, exceptions, () -> client.createToStream(streamName, groupName).get());
+        Optional<PersistentSubscriptionToStreamInfo> info = Optional.empty();
+        for (int i = 0; i < 10; i++) {
+            info = client.getInfoToStream(streamName, groupName).get();
+
+            if (!info.isPresent()) {
+                Thread.sleep(500);
+                continue;
+            }
+
+            break;
+        }
+
+       Assertions.assertTrue(info.isPresent());
+       Assertions.assertEquals(info.get().getEventSource(), streamName);
+       Assertions.assertEquals(info.get().getGroupName(), groupName);
+    }
+
+    @Test
+    @Order(10)
+    default void testRestartSubsystem() throws Throwable {
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        client.restartSubsystem().get();
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionToAllWithFilterTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/PersistentSubscriptionToAllWithFilterTests.java
similarity index 60%
rename from db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionToAllWithFilterTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/PersistentSubscriptionToAllWithFilterTests.java
index e34f05c..f747f4b 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/PersistentSubscriptionToAllWithFilterTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/PersistentSubscriptionToAllWithFilterTests.java
@@ -1,27 +1,27 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.persistentsubscriptions;
 
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
 
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 
-public class PersistentSubscriptionToAllWithFilterTests extends ESDBTests {
+public interface PersistentSubscriptionToAllWithFilterTests extends ConnectionAware {
     @Test
-    public void testPersistentSubscriptionToAllWithFilter() throws Throwable {
-        EventStoreDBPersistentSubscriptionsClient client = getEmptyServer().getPersistentSubscriptionsClient();
-        EventStoreDBClient streamsClient = getEmptyServer().getClient();
+    default void testPersistentSubscriptionToAllWithFilter() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        EventStoreDBClient streamsClient = getDatabase().defaultClient();
         String groupName = generateName();
         int filteredEventTypeCount = 10;
         String filteredEventType = "filtered-event-type";
         String otherEventType = UUID.randomUUID().toString();
 
-        streamsClient.appendToStream(generateName(), generateEvents(5, otherEventType).iterator()).get();
-        streamsClient.appendToStream(generateName(), generateEvents(filteredEventTypeCount, filteredEventType).iterator()).get();
-        streamsClient.appendToStream(generateName(), generateEvents(10, otherEventType).iterator()).get();
+        flaky(10, exceptions, () -> streamsClient.appendToStream(generateName(), generateEvents(5, otherEventType).iterator()).get());
+        flaky(10, exceptions, () -> streamsClient.appendToStream(generateName(), generateEvents(filteredEventTypeCount, filteredEventType).iterator()).get());
+        flaky(10, exceptions, () -> streamsClient.appendToStream(generateName(), generateEvents(10, otherEventType).iterator()).get());
 
         final CountDownLatch receivedEvents = new CountDownLatch(filteredEventTypeCount);
         final CountDownLatch cancellation = new CountDownLatch(1);
@@ -54,18 +54,11 @@ public class PersistentSubscriptionToAllWithFilterTests extends ESDBTests {
             .addEventTypePrefix(filteredEventType)
             .build();
 
-        try {
-            client.createToAll(groupName, CreatePersistentSubscriptionToAllOptions.get()
-                            .fromStart()
-                            .filter(filter))
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-
-            return;
-        }
+        flaky(10, exceptions, () ->
+                client.createToAll(
+                        groupName,
+                        CreatePersistentSubscriptionToAllOptions.get().fromStart().filter(filter))
+                .get());
 
         PersistentSubscription result = client.subscribeToAll(groupName, listener)
             .get();
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/SubscribePersistentSubscriptionToStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/SubscribePersistentSubscriptionToStreamTests.java
similarity index 65%
rename from db-client-java/src/test/java/com/eventstore/dbclient/SubscribePersistentSubscriptionToStreamTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/SubscribePersistentSubscriptionToStreamTests.java
index f55945e..07c9ea0 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/SubscribePersistentSubscriptionToStreamTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/SubscribePersistentSubscriptionToStreamTests.java
@@ -1,58 +1,24 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.persistentsubscriptions;
 
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
 
-import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 
-public class SubscribePersistentSubscriptionToStreamTests extends ESDBTests {
-    private EventStoreDBClient streamsClient;
-    private EventStoreDBPersistentSubscriptionsClient client;
-
-    @BeforeEach
-    public void init() {
-        streamsClient = getEmptyServer().getClient();
-        client = getEmptyServer().getPersistentSubscriptionsClient();
-    }
-
-    class Foo {
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            SubscribePersistentSubscriptionToStreamTests.Foo foo1 = (SubscribePersistentSubscriptionToStreamTests.Foo) o;
-            return foo == foo1.foo;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(foo);
-        }
-
-        private boolean foo;
-
-        public boolean isFoo() {
-            return foo;
-        }
-
-        public void setFoo(boolean foo) {
-            this.foo = foo;
-        }
-    }
-
+public interface SubscribePersistentSubscriptionToStreamTests extends ConnectionAware {
     @Test
-    public void testSubscribePersistentSub() throws Throwable {
+    default void testSubscribePersistentSub() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
         String streamName = generateName();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        EventStoreDBClient streamsClient = getDatabase().defaultClient();
 
-        client.createToStream(streamName, "aGroup")
-                .get();
+        flaky(10, exceptions, () -> client.createToStream(streamName, "aGroup")
+                .get());
 
-        EventDataBuilder builder = EventData.builderAsJson("foobar", new SubscribePersistentSubscriptionToStreamTests.Foo());
+        EventDataBuilder builder = EventData.builderAsJson("foobar", new Foo());
 
         streamsClient.appendToStream(streamName, builder.build(), builder.build(), builder.build())
                 .get();
@@ -95,21 +61,16 @@ public class SubscribePersistentSubscriptionToStreamTests extends ESDBTests {
     }
 
     @Test
-    public void testSubscribePersistentSubToAll() throws Throwable {
+    default void testSubscribePersistentSubToAll() throws Throwable {
+        Exceptions exceptions = new Exceptions().registerGoAwayError();
         String streamName = generateName();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
+        EventStoreDBClient streamsClient = getDatabase().defaultClient();
 
-        try {
-            client.createToAll("aGroup")
-                    .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-
-            return;
-        }
+        flaky(10, exceptions, () -> client.createToAll("aGroup")
+                .get());
 
-        EventDataBuilder builder = EventData.builderAsJson("foobar", new SubscribePersistentSubscriptionToStreamTests.Foo());
+        EventDataBuilder builder = EventData.builderAsJson("foobar", new Foo());
 
         streamsClient.appendToStream(streamName, builder.build(), builder.build(), builder.build())
                 .get();
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/UpdatePersistentSubscriptionToStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/UpdatePersistentSubscriptionToStreamTests.java
similarity index 52%
rename from db-client-java/src/test/java/com/eventstore/dbclient/UpdatePersistentSubscriptionToStreamTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/UpdatePersistentSubscriptionToStreamTests.java
index 8882d33..a839752 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/UpdatePersistentSubscriptionToStreamTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/persistentsubscriptions/UpdatePersistentSubscriptionToStreamTests.java
@@ -1,25 +1,16 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.persistentsubscriptions;
 
-import org.junit.jupiter.api.BeforeEach;
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
-import testcontainers.module.EventStoreDB;
-
 import java.util.concurrent.ExecutionException;
 
-public class UpdatePersistentSubscriptionToStreamTests extends ESDBTests {
-    private EventStoreDBPersistentSubscriptionsClient client;
-
-    @BeforeEach
-    public void init() {
-        client = getEmptyServer().getPersistentSubscriptionsClient();
-    }
-
+public interface UpdatePersistentSubscriptionToStreamTests extends ConnectionAware {
     @Test
-    public void testUpdatePersistentSub() throws Throwable {
+    default void testUpdatePersistentSub() throws Throwable {
         String streamName = generateName();
         String groupName = generateName();
 
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
         client.createToStream(streamName, groupName)
                 .get();
 
@@ -32,20 +23,12 @@ public class UpdatePersistentSubscriptionToStreamTests extends ESDBTests {
     }
 
     @Test
-    public void testUpdatePersistentSubToAll() throws Throwable {
+    default void testUpdatePersistentSubToAll() throws Throwable {
         String groupName = generateName();
+        EventStoreDBPersistentSubscriptionsClient client = EventStoreDBPersistentSubscriptionsClient.from(getDatabase().defaultClient());
 
-        try {
-            client.createToAll(groupName)
+        client.createToAll(groupName)
                     .get();
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof UnsupportedFeatureException && !EventStoreDB.isTestedAgainstVersion20()) {
-                throw e;
-            }
-
-            return;
-        }
-
         UpdatePersistentSubscriptionToAllOptions updatedSettings = UpdatePersistentSubscriptionToAllOptions.get()
                 .checkpointAfterInMs(5_000)
                 .startFrom(3,4);
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/samples/projection_management/ProjectionManagement.java b/db-client-java/src/test/java/com/eventstore/dbclient/samples/projection_management/ProjectionManagement.java
index 65e2fdf..7161761 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/samples/projection_management/ProjectionManagement.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/samples/projection_management/ProjectionManagement.java
@@ -2,34 +2,37 @@ package com.eventstore.dbclient.samples.projection_management;
 
 import com.eventstore.dbclient.*;
 import org.junit.*;
-import testcontainers.module.ESDBTests;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 
-public class ProjectionManagement extends ESDBTests {
-
-    private EventStoreDBProjectionManagementClient client;
-
+public class ProjectionManagement {
 
+    @Test
     public void testProjectionManagementSamples() throws Throwable {
 
-        client = getEmptyServer().getProjectionClient();
-        disable(client);
-        disableNotFound(client);
-        enable(client);
-        enableNotFound(client);
-        delete(client);
-        deleteNotFound(client);
-        abort(client);
-        abortNotFound(client);
-        reset(client);
-        resetNotFound(client);
-        create(client);
-        createConflict(client);
-        update(client);
-        updateNotFound(client);
-        restartSubSystem(client);
+        Database database = DatabaseFactory.spawn();
+        try {
+            EventStoreDBProjectionManagementClient client = EventStoreDBProjectionManagementClient.from(database.defaultClient());
+            disable(client);
+            disableNotFound(client);
+            enable(client);
+            enableNotFound(client);
+            delete(client);
+            deleteNotFound(client);
+            abort(client);
+            abortNotFound(client);
+            reset(client);
+            resetNotFound(client);
+            create(client);
+            createConflict(client);
+            update(client);
+            updateNotFound(client);
+            restartSubSystem(client);
+        } finally {
+            database.dispose();
+        }
     }
 
     private static EventStoreDBProjectionManagementClient createClient(String connection) {
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/AppendTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/AppendTests.java
similarity index 71%
rename from db-client-java/src/test/java/com/eventstore/dbclient/AppendTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/streams/AppendTests.java
index 2091d7d..3a44cdf 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/AppendTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/AppendTests.java
@@ -1,43 +1,17 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.streams;
 
+import com.eventstore.dbclient.*;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
-import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 
-class Foo {
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        Foo foo1 = (Foo) o;
-        return foo == foo1.foo;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(foo);
-    }
-
-    private boolean foo;
-
-    public boolean isFoo() {
-        return foo;
-    }
-
-    public void setFoo(boolean foo) {
-        this.foo = foo;
-    }
-}
-
-public class AppendTests extends ESDBTests {
+public interface AppendTests extends ConnectionAware {
     @Test
-    public void testAppendSingleEventNoStream() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClient();
+    default void testAppendSingleEventNoStream() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
 
         final String streamName = generateName();
         final String eventType = "TestEvent";
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/ClientLifecycleTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/ClientLifecycleTests.java
similarity index 58%
rename from db-client-java/src/test/java/com/eventstore/dbclient/ClientLifecycleTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/streams/ClientLifecycleTests.java
index 67b1d44..f097782 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/ClientLifecycleTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/ClientLifecycleTests.java
@@ -1,29 +1,30 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.streams;
 
+import com.eventstore.dbclient.ConnectionAware;
+import com.eventstore.dbclient.ConnectionShutdownException;
+import com.eventstore.dbclient.EventStoreDBClient;
+import com.eventstore.dbclient.EventStoreDBClientSettings;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
-public class ClientLifecycleTests extends ESDBTests {
+public interface ClientLifecycleTests extends ConnectionAware {
     @Test
-    public void testProvidesRunningStatus() {
-        EventStoreDBClientSettings settings = getEmptyServer().getSettings();
-
-        EventStoreDBClient client = EventStoreDBClient.create(settings);
+    default void testProvidesRunningStatus() {
+        EventStoreDBClient client = getDatabase().newClient();
 
         assertFalse(client.isShutdown());
     }
 
     @Test
-    public void testProvidesShutdownStatusAfterManualShutdown() throws Throwable {
-        EventStoreDBClientSettings settings = getEmptyServer().getSettings();
-        EventStoreDBClient client = EventStoreDBClient.create(settings);
+    default void testProvidesShutdownStatusAfterManualShutdown() throws Throwable {
+        EventStoreDBClient client = getDatabase().newClient();
 
         client.shutdown().get();
 
@@ -31,7 +32,7 @@ public class ClientLifecycleTests extends ESDBTests {
     }
 
     @Test
-    public void testProvidesShutdownStatusAfterAutomaticShutdown() throws Throwable {
+    default void testProvidesShutdownStatusAfterAutomaticShutdown() throws Throwable {
         EventStoreDBClientSettings settings = EventStoreDBClientSettings.builder()
                 .addHost("unknown.host.name", 2113)
                 .buildConnectionSettings();
@@ -45,5 +46,4 @@ public class ClientLifecycleTests extends ESDBTests {
         }
         assertTrue(client.isShutdown());
     }
-
 }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/DeadlineTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/DeadlineTests.java
similarity index 68%
rename from db-client-java/src/test/java/com/eventstore/dbclient/DeadlineTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/streams/DeadlineTests.java
index 7a51ed5..bade242 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/DeadlineTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/DeadlineTests.java
@@ -1,18 +1,21 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.streams;
 
+import com.eventstore.dbclient.*;
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
+import org.junitpioneer.jupiter.RetryingTest;
 
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 
-public class DeadlineTests extends ESDBTests {
+public interface DeadlineTests extends ConnectionAware {
     @Test
-    public void testDefaultDeadline() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClientWithSettings("tls=false&defaultdeadline=1");
+     default void testDefaultDeadline() throws Throwable {
+        EventStoreDBClient client = getDatabase().connectWith(opts ->
+                opts.defaultDeadline(1)
+                        .maxDiscoverAttempts(3));
         UUID id = UUID.randomUUID();
 
         EventData data = EventDataBuilder.binary(id, "type", new byte[]{}).build();
@@ -20,13 +23,11 @@ public class DeadlineTests extends ESDBTests {
         StatusRuntimeException status = (StatusRuntimeException) e.getCause();
 
         Assertions.assertEquals(status.getStatus().getCode(), Status.Code.DEADLINE_EXCEEDED);
-
-        client.shutdown();
     }
 
-    @Test
-    public void testOptionLevelDeadline() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClientWithSettings("tls=false");
+    @RetryingTest(3)
+    default void testOptionLevelDeadline() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
         UUID id = UUID.randomUUID();
 
         EventData data = EventDataBuilder.binary(id, "type", new byte[]{}).build();
@@ -35,7 +36,5 @@ public class DeadlineTests extends ESDBTests {
         StatusRuntimeException status = (StatusRuntimeException) e.getCause();
 
         Assertions.assertEquals(status.getStatus().getCode(), Status.Code.DEADLINE_EXCEEDED);
-
-        client.shutdown();
     }
 }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/DeleteTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/DeleteTests.java
similarity index 58%
rename from db-client-java/src/test/java/com/eventstore/dbclient/DeleteTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/streams/DeleteTests.java
index 42f4448..57f9bd2 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/DeleteTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/DeleteTests.java
@@ -1,16 +1,15 @@
-package com.eventstore.dbclient;
-
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
+package com.eventstore.dbclient.streams;
 
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
-public class DeleteTests extends ESDBTests {
+import java.util.concurrent.ExecutionException;
+
+public interface DeleteTests extends ConnectionAware {
     @Test
-    public void testCanDeleteStream() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClient();
+    default void testCanDeleteStream() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
         String streamName = generateName();
 
         client.appendToStream(streamName, generateEvents(1, "foobar").iterator()).get();
@@ -19,8 +18,8 @@ public class DeleteTests extends ESDBTests {
     }
 
     @Test
-    public void testDeleteStreamWhenAlreadyDeleted() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClient();
+    default void testDeleteStreamWhenAlreadyDeleted() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
         String streamName = generateName();
 
         client.appendToStream(streamName, generateEvents(1, "foobar").iterator()).get();
@@ -35,15 +34,8 @@ public class DeleteTests extends ESDBTests {
     }
 
     @Test
-    public void testDeleteStreamWhenDoesntExist() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClient();
-        Optional<ServerVersion> serverVersion = client.getGrpcClient().getServerVersion().get();
-
-        // There are some versions of the server that will not send wrong expected version in this case.
-        // Those version just send gRPC status Unknown in this case.
-        if (!serverVersion.isPresent() || serverVersion.get().getMajor() <= 20) {
-            return;
-        }
+    default void testDeleteStreamWhenDoesntExist() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
 
         String streamName = generateName();
         DeleteStreamOptions options = DeleteStreamOptions.get()
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/InterceptorTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/InterceptorTests.java
similarity index 67%
rename from db-client-java/src/test/java/com/eventstore/dbclient/InterceptorTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/streams/InterceptorTests.java
index 894790d..25fa37b 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/InterceptorTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/InterceptorTests.java
@@ -1,22 +1,20 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.streams;
 
+import com.eventstore.dbclient.ConnectionAware;
+import com.eventstore.dbclient.EventStoreDBClient;
+import com.eventstore.dbclient.ReadStreamOptions;
 import io.grpc.*;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
-public class InterceptorTests extends ESDBTests {
+public interface InterceptorTests extends ConnectionAware {
     @Test
-    public void testInterceptorIsCalled() {
-        EventStoreDBClientSettings settings = getEmptyServer().getSettings();
-
+    default void testInterceptorIsCalled() {
         AtomicInteger atom = new AtomicInteger(0);
+        EventStoreDBClient client = getDatabase().connectWith(opts -> opts.addInterceptor(new MyInterceptor(atom)));
 
-        settings.getInterceptors().add(new MyInterceptor(atom));
-
-        EventStoreDBClient client = EventStoreDBClient.create(settings);
         try {
             client.readStream("foobar", ReadStreamOptions.get()).get();
         } catch (Exception e) {
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/MetadataTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java
similarity index 76%
rename from db-client-java/src/test/java/com/eventstore/dbclient/MetadataTests.java
rename to db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java
index 112278b..32d963e 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/MetadataTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java
@@ -1,15 +1,15 @@
-package com.eventstore.dbclient;
+package com.eventstore.dbclient.streams;
 
+import com.eventstore.dbclient.*;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import testcontainers.module.ESDBTests;
 
 import java.util.HashMap;
 
-public class MetadataTests extends ESDBTests {
+public interface MetadataTests extends ConnectionAware {
     @Test
-    public void testSetStreamMetadata() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClient();
+    default void testSetStreamMetadata() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
 
         StreamMetadata metadata = new StreamMetadata();
 
@@ -40,8 +40,8 @@ public class MetadataTests extends ESDBTests {
     }
 
     @Test
-    public void testReadNoExistingMetadata() throws Throwable {
-        EventStoreDBClient client = getEmptyServer().getClient();
+    default void testReadNoExistingMetadata() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
         String streamName = generateName();
         client.appendToStream(streamName, EventDataBuilder.json("bar", new HashMap<String, Object>()).build()).get();
 
@@ -49,4 +49,4 @@ public class MetadataTests extends ESDBTests {
 
         Assertions.assertEquals(new StreamMetadata(), got);
     }
-}
+}
\ No newline at end of file
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/streams/ReadStreamTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/ReadStreamTests.java
new file mode 100644
index 0000000..b8ed09a
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/ReadStreamTests.java
@@ -0,0 +1,54 @@
+package com.eventstore.dbclient.streams;
+
+import com.eventstore.dbclient.*;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public interface ReadStreamTests extends ConnectionAware {
+    @Test
+    default void testReadStreamEvents() throws Throwable {
+        String streamName = generateName();
+        int count = 3;
+        List<BazEvent> expecteds = generateBazEvent(count);
+        List<BazEvent> actuals = new ArrayList<>();
+        List<EventData> events = new ArrayList<>();
+
+        for (BazEvent event : expecteds) {
+            events.add(serializeBazEvent(event));
+        }
+
+        EventStoreDBClient client = getDefaultClient();
+
+        client.appendToStream(streamName, events.iterator()).get();
+        ReadResult result = client.readStream(streamName, ReadStreamOptions.get()).get();
+
+        for (ResolvedEvent resolvedEvent : result.getEvents()) {
+            BazEvent event = deserializeBazEvent(resolvedEvent.getOriginalEvent().getEventData());
+            actuals.add(event);
+        }
+
+        for (int i = 0; i < count; i++) {
+            BazEvent actual = actuals.get(i);
+            BazEvent expected = expecteds.get(i);
+            Assertions.assertEquals(expected.getName(), actual.getName());
+            Assertions.assertEquals(expected.getAge(), actual.getAge());
+        }
+    }
+
+    @Test
+    default void testNonexistentStream() throws Throwable {
+        String streamName = generateName();
+
+        Assertions.assertThrows(StreamNotFoundException.class, () -> {
+            try {
+                getDefaultClient().readStream(streamName, ReadStreamOptions.get()).get();
+            } catch (ExecutionException e) {
+                throw e.getCause();
+            }
+        });
+    }
+}
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/streams/SubscriptionTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/SubscriptionTests.java
new file mode 100644
index 0000000..31b382a
--- /dev/null
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/SubscriptionTests.java
@@ -0,0 +1,138 @@
+package com.eventstore.dbclient.streams;
+
+import com.eventstore.dbclient.*;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public interface SubscriptionTests extends ConnectionAware {
+    @Test
+    @Timeout(value = 2, unit = TimeUnit.MINUTES)
+    default void testSubscription() throws Throwable {
+        final String streamName = generateName();
+        final ArrayList<BazEvent> before = generateBazEvent(3);
+        final ArrayList<BazEvent> after = generateBazEvent(3);
+        final ArrayList<BazEvent> expected = new ArrayList<>();
+
+        expected.addAll(before);
+        expected.addAll(after);
+
+        getDefaultClient().appendToStream(streamName, serializeBazEvents(before).iterator()).get();
+        final CountDownLatch receivedEvents = new CountDownLatch(before.size());
+        final CountDownLatch appendedEvents = new CountDownLatch(after.size());
+        final AtomicInteger count = new AtomicInteger(0);
+
+        getLogger().debug("Before subscribing to stream");
+        Subscription subscription = getDefaultClient().subscribeToStream(streamName, new SubscriptionListener() {
+            @Override
+            public void onEvent(Subscription subscription, ResolvedEvent event) {
+                try {
+                    BazEvent source = expected.get(count.getAndIncrement());
+                    BazEvent actual = deserializeBazEvent(event.getOriginalEvent().getEventData());
+
+                    Assertions.assertEquals(source.getAge(), actual.getAge());
+                    Assertions.assertEquals(source.getName(), actual.getName());
+                } catch (IOException e) {
+                    getLogger().error("Error when handling subscription", e);
+                    Assertions.fail(e);
+                }
+
+                if (count.get() <= before.size()) {
+                    receivedEvents.countDown();
+                } else {
+                    appendedEvents.countDown();
+                }
+            }
+
+            @Override
+            public void onCancelled(Subscription subscription) {
+            }
+
+            @Override
+            public void onError(Subscription subscription, Throwable throwable) {
+                getLogger().error("Error when running subscription", throwable);
+                Assertions.fail(throwable);
+            }
+        }).get();
+        getLogger().debug("After subscribing to stream");
+
+        getLogger().debug("Waiting for receiving events to come along");
+        receivedEvents.await();
+        getLogger().debug("Done");
+
+        getDefaultClient().appendToStream(streamName, serializeBazEvents(after).iterator()).get();
+
+        getLogger().debug("Waiting for appending events to come along");
+        appendedEvents.await();
+        getLogger().debug("Done");
+        Assertions.assertEquals(expected.size(), count.get());
+        getLogger().debug("Before stopping subscription");
+        subscription.stop();
+        getLogger().debug("Done");
+    }
+
+    @Test
+    @Timeout(value = 2, unit = TimeUnit.MINUTES)
+    default void testSubscriptionToAllWithFilter() throws Throwable {
+        String eventType = generateName();
+        final ArrayList<BazEvent> before = generateBazEvent(3);
+        final ArrayList<BazEvent> after = generateBazEvent(3);
+        final ArrayList<BazEvent> expected = new ArrayList<>();
+
+        expected.addAll(before);
+        expected.addAll(after);
+
+        getDefaultClient().appendToStream(generateName(), serializeBazEvents(before, eventType).iterator()).get();
+        final CountDownLatch receivedEvents = new CountDownLatch(before.size());
+        final CountDownLatch appendedEvents = new CountDownLatch(after.size());
+        final AtomicInteger count = new AtomicInteger(0);
+        Subscription subscription = getDefaultClient().subscribeToAll(new SubscriptionListener() {
+            @Override
+            public void onEvent(Subscription subscription, ResolvedEvent event) {
+                try {
+                    BazEvent source = expected.get(count.getAndIncrement());
+                    BazEvent actual = deserializeBazEvent(event.getOriginalEvent().getEventData());
+
+                    Assertions.assertEquals(source.getAge(), actual.getAge());
+                    Assertions.assertEquals(source.getName(), actual.getName());
+                } catch (IOException e) {
+                    getLogger().error("Error when handling subscription", e);
+                    throw new RuntimeException(e);
+                }
+
+                if (count.get() <= before.size()) {
+                    receivedEvents.countDown();
+                } else {
+                    appendedEvents.countDown();
+                }
+            }
+
+            @Override
+            public void onCancelled(Subscription subscription) {
+            }
+
+            @Override
+            public void onError(Subscription subscription, Throwable throwable) {
+                Assertions.fail(throwable);
+            }
+        }, SubscribeToAllOptions.get()
+                .filter(SubscriptionFilter
+                        .newBuilder()
+                        .addEventTypePrefix(eventType)
+                        .build())).get();
+
+        receivedEvents.await();
+
+        getDefaultClient().appendToStream(generateName(), serializeBazEvents(after, eventType).iterator()).get();
+
+        appendedEvents.await();
+        Assertions.assertEquals(expected.size(), count.get());
+        subscription.stop();
+    }
+}
diff --git a/db-client-java/src/test/java/extensions/BeforeEverythingElseExtension.java b/db-client-java/src/test/java/extensions/BeforeEverythingElseExtension.java
deleted file mode 100644
index a66d170..0000000
--- a/db-client-java/src/test/java/extensions/BeforeEverythingElseExtension.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package extensions;
-
-import org.junit.jupiter.api.extension.BeforeAllCallback;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import testcontainers.module.EventStoreDB;
-
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-public class BeforeEverythingElseExtension implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {
-    private static boolean started = false;
-    private static EventStoreDB EMPTY_SERVER;
-    private static EventStoreDB POPULATED_SERVER;
-
-    final static Lock lock = new ReentrantLock();
-
-    @Override
-    public void beforeAll(ExtensionContext context) throws Exception {
-        lock.lock();
-        if (!started) {
-            started = true;
-            EMPTY_SERVER = new EventStoreDB(true);
-            POPULATED_SERVER = new EventStoreDB(false);
-            // Your "before all tests" startup logic goes here
-            // The following line registers a callback hook when the root test context is
-            // shut down
-
-            // do your work - which might take some time -
-            // or just uses more time than the simple check of a boolean
-        }
-        // free the access
-        lock.unlock();
-    }
-
-    @Override
-    public void close() throws Throwable {
-        EMPTY_SERVER.shutdownClients();
-        POPULATED_SERVER.shutdownClients();
-        EMPTY_SERVER.stop();
-        POPULATED_SERVER.stop();
-    }
-
-    public static EventStoreDB GetEmptyServer() {
-        return EMPTY_SERVER;
-    }
-
-    public static EventStoreDB GetPopulatedServer() {
-        return POPULATED_SERVER;
-    }
-}
diff --git a/db-client-java/src/test/java/testcontainers/module/ESDBTests.java b/db-client-java/src/test/java/testcontainers/module/ESDBTests.java
deleted file mode 100644
index be580fd..0000000
--- a/db-client-java/src/test/java/testcontainers/module/ESDBTests.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package testcontainers.module;
-
-import com.eventstore.dbclient.EventData;
-import com.eventstore.dbclient.EventDataBuilder;
-import com.github.javafaker.Faker;
-import extensions.BeforeEverythingElseExtension;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-
-import java.util.ArrayList;
-import java.util.UUID;
-
-public class ESDBTests {
-    private final Faker faker = new Faker();
-
-    public static EventStoreDB getEmptyServer() {
-        return BeforeEverythingElseExtension.GetEmptyServer();
-    }
-
-    public static EventStoreDB getPopulatedServer() {
-        return BeforeEverythingElseExtension.GetPopulatedServer();
-    }
-
-    protected String generateName() {
-        return String.format("%s-%s", faker.pokemon().name(), faker.pokemon().name());
-    }
-
-    protected ArrayList<EventData> generateEvents(int amount, String eventType) {
-        ArrayList<EventData> events = new ArrayList<>();
-        for (int i = 0; i < amount; i++) {
-            events.add(EventDataBuilder.json(UUID.randomUUID(), eventType, new byte[]{}).build());
-        }
-
-        return events;
-    }
-}
diff --git a/db-client-java/src/test/java/testcontainers/module/EventStoreDB.java b/db-client-java/src/test/java/testcontainers/module/EventStoreDB.java
deleted file mode 100644
index ff3f741..0000000
--- a/db-client-java/src/test/java/testcontainers/module/EventStoreDB.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package testcontainers.module;
-
-import com.eventstore.dbclient.*;
-import com.github.dockerjava.api.model.HealthCheck;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.wait.strategy.Wait;
-
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-
-public class EventStoreDB extends GenericContainer<EventStoreDB> {
-    public static final String NAME;
-    public static final String IMAGE;
-    public static final String IMAGE_TAG;
-    public static final HealthCheck HEALTH_CHECK;
-    private static final int DB_HTTP_PORT;
-
-    static {
-        NAME = "testdata";
-        IMAGE = "ghcr.io/eventstore/" + NAME;
-        IMAGE_TAG = "ci";
-        HEALTH_CHECK = new HealthCheck()
-                .withInterval(1000000000L)
-                .withTimeout(1000000000L)
-                .withRetries(10);
-        DB_HTTP_PORT = 2113;
-    }
-
-    private static String getImageName() {
-        String tag = Optional.ofNullable(System.getenv("EVENTSTORE_DOCKER_TAG_ENV")).orElse(IMAGE_TAG);
-        return Optional.ofNullable(System.getenv("EVENTSTORE_IMAGE"))
-                .orElse(IMAGE + ":" + tag);
-    }
-
-    public static boolean isTestedAgainstVersion20() {
-        return Optional.ofNullable(System.getenv("EVENTSTORE_DOCKER_TAG_ENV")).orElse(IMAGE_TAG).startsWith("20");
-    }
-
-    public static boolean isTestedAgains20_10() {
-        return Optional.ofNullable(System.getenv("EVENTSTORE_DOCKER_TAG_ENV")).orElse(IMAGE_TAG).startsWith("20.10");
-    }
-
-    private final EventStoreDBClientSettings settings;
-    private final EventStoreDBClient client;
-    private final EventStoreDBPersistentSubscriptionsClient persistentSubscriptionsClient;
-    private final EventStoreDBProjectionManagementClient projectionClient;
-
-    public EventStoreDB(boolean emptyDatabase) {
-        super(getImageName());
-
-        addExposedPorts(1113, 2113);
-
-        withEnv("EVENTSTORE_RUN_PROJECTIONS", "ALL");
-
-        withEnv("EVENTSTORE_INSECURE", "true");
-        if (!emptyDatabase) {
-            withEnv("EVENTSTORE_MEM_DB", "false");
-            withEnv("EVENTSTORE_DB", "/data/integration-tests");
-        }
-
-        withCreateContainerCmdModifier(cmd -> cmd.withHealthcheck(HEALTH_CHECK));
-        waitingFor(Wait.forHealthcheck());
-
-        start();
-
-        settings = getEventStoreDBClientSettings();
-        client = EventStoreDBClient.create(settings);
-        persistentSubscriptionsClient = EventStoreDBPersistentSubscriptionsClient.create(settings);
-        projectionClient = EventStoreDBProjectionManagementClient.create(settings);
-    }
-
-    private EventStoreDBClientSettings getEventStoreDBClientSettings() {
-        return EventStoreDBConnectionString.parseOrThrow(getConnectionString());
-    }
-
-    private String getConnectionString() {
-        final String address = getContainerIpAddress();
-        final int port = getMappedPort(DB_HTTP_PORT);
-
-        return String.format("esdb://%s:%d?tls=false&defaultdeadline=60000", address, port);
-    }
-
-    public EventStoreDBClientSettings getSettings() {
-        return settings;
-    }
-
-    public EventStoreDBClient getClient() {
-        return client;
-    }
-
-    public EventStoreDBClient getClientWithSettings(String args) {
-        final String address = getContainerIpAddress();
-        final int port = getMappedPort(DB_HTTP_PORT);
-
-        String connStr =  String.format("esdb://%s:%d?%s", address, port, args);
-        return EventStoreDBClient.create(EventStoreDBConnectionString.parseOrThrow(connStr));
-    }
-
-    public EventStoreDBPersistentSubscriptionsClient getPersistentSubscriptionsClient() {
-        return persistentSubscriptionsClient;
-    }
-
-    public EventStoreDBProjectionManagementClient getProjectionClient() {
-        return projectionClient;
-    }
-
-    public void shutdownClients() {
-        try {
-            client.shutdown().get();
-            persistentSubscriptionsClient.shutdown().get();
-            projectionClient.shutdown().get();
-        } catch (ExecutionException | InterruptedException e) {
-            e.printStackTrace();
-        }
-    }
-}
diff --git a/db-client-java/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/db-client-java/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
deleted file mode 100644
index 56f08fe..0000000
--- a/db-client-java/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
+++ /dev/null
@@ -1,1 +0,0 @@
-extensions.BeforeEverythingElseExtension
\ No newline at end of file
diff --git a/db-client-java/src/test/resources/simplelogger.properties b/db-client-java/src/test/resources/simplelogger.properties
index f245a54..f73f8cd 100644
--- a/db-client-java/src/test/resources/simplelogger.properties
+++ b/db-client-java/src/test/resources/simplelogger.properties
@@ -1,1 +1,2 @@
-org.slf4j.simpleLogger.log.com.eventstore=debug
\ No newline at end of file
+org.slf4j.simpleLogger.defaultLogLevel=error
+org.slf4j.simpleLogger.log.com.eventstore.dbclient=debug
\ No newline at end of file