+ * When applied at the class level, all test methods within that class will be enabled on presence of the specified + * class. + *
+ * If a test method is disabled via this annotation, that does not prevent the test class from being instantiated. + * Rather, it prevents the execution of the test method and method-level lifecycle callbacks such as {@code @BeforeEach} + * methods, {@code @AfterEach} methods, and corresponding extension APIs. + *
+ * This annotation may be used as a meta-annotation in order to create a custom composed annotation that
+ * inherits the semantics of this annotation.
+ *
+ * @see JRE
+ * @see org.junit.jupiter.api.Disabled
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@ExtendWith(EnabledOnClassCondition.class)
+public @interface EnabledOnClass {
+
+ String value();
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java b/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java
new file mode 100644
index 00000000..7fe3a602
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.testing;
+
+import static org.junit.jupiter.api.extension.ConditionEvaluationResult.*;
+import static org.junit.platform.commons.util.AnnotationUtils.*;
+
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import org.springframework.util.ClassUtils;
+
+/**
+ * {@link ExecutionCondition} for {@link EnabledOnClass @EnabledOnClass}.
+ *
+ * @author Mark Paluch
+ * @see EnabledOnClass
+ */
+class EnabledOnClassCondition implements ExecutionCondition {
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
+ return findAnnotation(context.getElement(), EnabledOnClass.class) //
+ .map(annotation -> isEnabled(annotation)
+ ? enabled(String.format("Class '%s' found on the class path.", annotation.value()))
+ : disabled(String.format("Class '%s' not found on the class path.", annotation.value()))) //
+ .orElseGet(this::enabledByDefault);
+ }
+
+ private boolean isEnabled(EnabledOnClass annotation) {
+ return ClassUtils.isPresent(annotation.value(), EnabledOnClassCondition.class.getClassLoader());
+ }
+
+ private ConditionEvaluationResult enabledByDefault() {
+ return enabled("@EnabledOnClass is not present");
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java b/src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java
new file mode 100644
index 00000000..7813a3db
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.testing;
+
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.ConnectionFactoryOptions;
+import io.r2dbc.spi.ConnectionFactoryProvider;
+
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * {@link ConnectionFactoryProvider} for Oracle's R2DBC driver. Allows for absence of the driver which is required when
+ * using Java 8 as the ServiceLoader requires presence of classes listed in the service loader manifest.
+ *
+ * @author Mark Paluch
+ */
+public class OracleConnectionFactoryProviderWrapper implements ConnectionFactoryProvider {
+
+ private final ConnectionFactoryProvider delegate;
+
+ public OracleConnectionFactoryProviderWrapper() {
+
+ if (ClassUtils.isPresent("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl", getClass().getClassLoader())) {
+
+ delegate = createProvider();
+ } else {
+ delegate = null;
+ }
+
+ }
+
+ private static ConnectionFactoryProvider createProvider() {
+
+ try {
+ return (ConnectionFactoryProvider) Class.forName("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl")
+ .newInstance();
+ } catch (ReflectiveOperationException e) {
+ ReflectionUtils.handleReflectionException(e);
+ }
+ return null;
+ }
+
+ @Override
+ public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) {
+ if (delegate != null) {
+ return delegate.create(connectionFactoryOptions);
+ }
+ throw new IllegalStateException(
+ "Oracle R2DBC (oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl) is not on the class path");
+ }
+
+ @Override
+ public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) {
+
+ if (delegate != null) {
+ return delegate.supports(connectionFactoryOptions);
+ }
+ return false;
+ }
+
+ @Override
+ public String getDriver() {
+
+ if (delegate != null) {
+ return delegate.getDriver();
+ }
+
+ return "oracle-r2dbc (proxy)";
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java
new file mode 100644
index 00000000..2997fb7b
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.testing;
+
+import io.r2dbc.spi.ConnectionFactories;
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.ConnectionFactoryOptions;
+
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import javax.sql.DataSource;
+
+import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.util.ClassUtils;
+
+import org.testcontainers.containers.OracleContainer;
+
+/**
+ * Utility class for testing against Oracle.
+ *
+ * @author Mark Paluch
+ */
+public class OracleTestSupport {
+
+ private static ExternalDatabase testContainerDatabase;
+
+ public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" //
+ + " id INTEGER PRIMARY KEY,\n" //
+ + " version INTEGER NULL,\n" //
+ + " name VARCHAR2(255) NOT NULL,\n" //
+ + " manual INTEGER NULL,\n" //
+ + " cert RAW(255) NULL\n" //
+ + ")";
+
+ public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" //
+ + " id INTEGER GENERATED by default on null as IDENTITY PRIMARY KEY,\n" //
+ + " version INTEGER NULL,\n" //
+ + " name VARCHAR2(255) NOT NULL,\n" //
+ + " manual INTEGER NULL\n" //
+ + ")";
+
+ /**
+ * Returns a database either hosted locally or running inside Docker.
+ *
+ * @return information about the database. Guaranteed to be not {@literal null}.
+ */
+ public static ExternalDatabase database() {
+
+ if (!ClassUtils.isPresent("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl",
+ OracleTestSupport.class.getClassLoader())) {
+ return ExternalDatabase.unavailable();
+ }
+
+ if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) {
+
+ return getFirstWorkingDatabase( //
+ OracleTestSupport::local, //
+ OracleTestSupport::testContainer //
+ );
+ } else {
+
+ return getFirstWorkingDatabase( //
+ OracleTestSupport::testContainer, //
+ OracleTestSupport::local //
+ );
+ }
+ }
+
+ @SafeVarargs
+ private static ExternalDatabase getFirstWorkingDatabase(Supplier