diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.micronaut-test-suite.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.micronaut-test-suite.gradle new file mode 100644 index 000000000..b965e9bb6 --- /dev/null +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.micronaut-test-suite.gradle @@ -0,0 +1,3 @@ +repositories { + mavenCentral() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44a1923db..cdb1f710b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -micronaut-docs = "2.0.0" micronaut = "4.1.9" - +micronaut-platform = "4.1.4" +micronaut-docs = "2.0.0" groovy = "4.0.15" managed-assertj = "3.24.2" @@ -15,9 +15,13 @@ managed-spock = "2.3-groovy-4.0" kotlin = "1.9.10" graal-svm = "23.1.1" +testcontainers = "1.19.1" +micronaut-test = "4.0.2" micronaut-data = "4.1.4" micronaut-hibernate-validator = "4.0.2" +micronaut-logging = "1.1.2" +micronaut-r2dbc = "5.0.1" micronaut-serde = "2.2.6" micronaut-spring = "5.0.2" micronaut-sql = "5.0.3" @@ -28,13 +32,16 @@ micronaut-gradle-plugin = "4.1.1" [libraries] # Core micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' } +micronaut-platform = { module = 'io.micronaut.platform:micronaut-platform', version.ref = 'micronaut-platform' } micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref = "micronaut-data" } micronaut-hibernate-validator = { module = "io.micronaut.beanvalidation:micronaut-hibernate-validator", version.ref = "micronaut-hibernate-validator" } +micronaut-r2dbc = { module = "io.micronaut.r2dbc:micronaut-r2dbc-bom", version.ref = "micronaut-r2dbc" } micronaut-serde = { module = "io.micronaut.serde:micronaut-serde-bom", version.ref = "micronaut-serde" } micronaut-spring = { module = "io.micronaut.spring:micronaut-spring-bom", version.ref = "micronaut-spring" } micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" } micronaut-reactor = { module = 'io.micronaut.reactor:micronaut-reactor-bom', version.ref = "micronaut-reactor" } +micronaut-test = { module = 'io.micronaut.test:micronaut-test-bom', version.ref = "micronaut-test" } managed-assertj-core = { module = "org.assertj:assertj-core", version.ref = "managed-assertj" } managed-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "managed-hamcrest" } @@ -66,3 +73,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = groovy = { module = "org.apache.groovy:groovy" } kotlin-gradle-plugin = { module = 'org.jetbrains.kotlin:kotlin-gradle-plugin', version.ref = 'kotlin' } + +testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" } +testcontainers = { module = "org.testcontainers:testcontainers" } +testcontainers-postgresql = { module = "org.testcontainers:postgresql"} diff --git a/settings.gradle b/settings.gradle index b5501e058..2c852f597 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,8 @@ micronautBuild { importMicronautCatalog("micronaut-spring") importMicronautCatalog("micronaut-sql") importMicronautCatalog("micronaut-reactor") + importMicronautCatalog("micronaut-test") + importMicronautCatalog("micronaut-r2dbc") } rootProject.name = 'test-parent' @@ -29,3 +31,5 @@ include "test-spock" include "test-junit5" include "test-kotest5" include "test-rest-assured" +include 'test-suite-sql-r2dbc' +include 'test-suite-at-sql-jpa' diff --git a/src/main/docs/guide/sql.adoc b/src/main/docs/guide/sql.adoc new file mode 100644 index 000000000..b6821aafc --- /dev/null +++ b/src/main/docs/guide/sql.adoc @@ -0,0 +1,87 @@ +When performing a test with a backing database, often some data is required in the database prior to running the tests. +As of `micronaut-test` version 4.1.0, there is an annotation api:test.annotation.Sql[]. + +This annotation can be used to specify the location of one or more sql files to be executed at one of four phases in your test execution: + +* `BEFORE_CLASS` - executed once before the tests are run (the default). +* `BEFORE_METHOD` - executed before each test method. +* `AFTER_METHOD` - executed after each test method. +* `AFTER_CLASS` - executed once after all the tests are run. + +The files are executed in the order specified in the annotation. + +For example given the two SQL scripts + +.test/resources/create.sql +[source,sql] +---- +include::test-junit5/src/test/resources/create.sql[] +---- + +and + +.test/resources/datasource_1_insert.sql +[source,sql] +---- +include::test-junit5/src/test/resources/datasource_1_insert.sql[] +---- + +We can annotate a test to run these two scripts prior to the test. + +[source, java, role="multi-language-sample"] +---- +include::{junit5tests}/SqlDatasourceTest.java[tags="clazz"] +---- + +[source, groovy, role="multi-language-sample"] +---- +include::{spocktests}/SqlDatasourceSpec.groovy[tags="clazz"] +---- + +[source, kotlin, role="multi-language-sample"] +---- +include::{kotest5tests}/SqlDatasourceTest.kt[tags="clazz"] +---- + +<1> Specify the location of the SQL scripts to be executed for a DataSource with the name `default`. + +== Phases + +The default phase for the scripts to be executed is `BEFORE_CLASS`. +To run the scripts at a different phase, we can specify the `phase` attribute of the annotation. + +[source, java] +---- +include::test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsThenNoneTest.java[tags="rollback"] +---- +<1> A script to run after each test in the specification. + +== Named Datasources + +If you have multiple datasources configured, you can specify the datasource name to use for the SQL scripts. + +[source, java, role="multi-language-sample"] +---- +include::{junit5tests}/SqlNamedDatasourceTest.java[tags="clazz"] +---- + +[source, groovy, role="multi-language-sample"] +---- +include::{spocktests}/SqlNamedDatasourceSpec.groovy[tags="clazz"] +---- + +[source, kotlin, role="multi-language-sample"] +---- +include::{kotest5tests}/SqlNamedDatasourceTest.kt[tags="clazz"] +---- + +<1> Specify the location of the SQL scripts to be executed for a DataSource with the given name. + +== R2DBC + +For R2DBC, the `Sql` annotation can be used in the same way as for JDBC however it is required to pass the `resourceType` as `ConnectionFactory.class`. + +[source,java] +---- +include::test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/MySqlConnectionTest.java[tags="clazz"] +---- diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 7142d33d4..30cc4eb4a 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -38,5 +38,6 @@ kotest5: kotest5ConstructorInjectionCaveats: Constructor Injection Caveats kotest5RefreshingInjectedBeansBasedOnRequiresUponPropertiesChanges: Refreshing injected beans based on `@Requires` upon properties changes restAssured: REST Assured +sql: Loading SQL before tests #spek: Testing with Spek repository: Repository diff --git a/test-core/build.gradle b/test-core/build.gradle index 39d374b7e..4473256df 100644 --- a/test-core/build.gradle +++ b/test-core/build.gradle @@ -6,6 +6,9 @@ plugins { dependencies { compileOnly(mn.micronaut.http.server) compileOnly(libs.junit.jupiter.api) + compileOnly(mnData.micronaut.data.connection.jdbc) + compileOnly(mnR2dbc.r2dbc.spi) + compileOnly(mn.reactor) api(mn.micronaut.inject) diff --git a/test-core/src/main/java/io/micronaut/test/annotation/Sql.java b/test-core/src/main/java/io/micronaut/test/annotation/Sql.java new file mode 100644 index 000000000..52f76a1da --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/annotation/Sql.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.core.annotation.Experimental; + +import javax.sql.DataSource; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be applied to a test scenario to execute SQL against a test database prior to the sceario being run. + * + * @since 4.1.0 + * @author Tim Yates + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Experimental +@Repeatable(Sql.Sqls.class) +public @interface Sql { + + /** + * @return The SQL scripts to execute + */ + @AliasFor(member = "scripts") + String[] value() default {}; + + /** + * The name of the datasource to use for the SQL scripts. + * + * @return The datasource name + */ + String dataSourceName() default "default"; + + /** + * The phase of the test to execute the SQL scripts. + * @return The phase + */ + Phase phase() default Phase.BEFORE_ALL; + + /** + * Scripts to execute, e.g. {@code "classpath:foo.sql"}. + * @return The SQL scripts to execute + */ + @AliasFor(member = "value") + String[] scripts() default {}; + + /** + * @return The type of the resource to use for the SQL scripts. + */ + Class resourceType() default DataSource.class; + + /** + * Wrapper annotation class to allow multiple Sql annotations per test class or method. + */ + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Inherited + @interface Sqls { + + /** + * @return The SQL scripts to execute + */ + Sql[] value(); + } + + /** + * The phase of the test to execute the SQL scripts. + */ + enum Phase { + /** + * Execute the SQL before all tests. + */ + BEFORE_ALL, + + /** + * Execute the SQL before each test. + */ + BEFORE_EACH, + + /** + * Execute the SQL after all tests. + */ + AFTER_ALL, + + /** + * Execute the SQL after each test. + */ + AFTER_EACH + } +} diff --git a/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java b/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java index 7b2e20333..ad7f239f0 100644 --- a/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java +++ b/test-core/src/main/java/io/micronaut/test/extensions/AbstractMicronautExtension.java @@ -37,6 +37,7 @@ import io.micronaut.runtime.context.scope.refresh.RefreshScope; import io.micronaut.test.annotation.AnnotationUtils; import io.micronaut.test.annotation.MicronautTestValue; +import io.micronaut.test.annotation.Sql; import io.micronaut.test.condition.TestActiveCondition; import io.micronaut.test.context.TestContext; import io.micronaut.test.context.TestExecutionListener; @@ -44,6 +45,7 @@ import io.micronaut.test.context.TestMethodInvocationContext; import io.micronaut.test.support.TestPropertyProvider; import io.micronaut.test.support.TestPropertyProviderFactory; +import io.micronaut.test.support.sql.TestSqlAnnotationHandler; import java.io.IOException; import java.io.InputStream; @@ -192,11 +194,17 @@ public void afterTestExecution(TestContext testContext) throws Exception { @Override public void beforeTestClass(TestContext testContext) throws Exception { fireListeners(TestExecutionListener::beforeTestClass, testContext, false); + if (specDefinition != null && applicationContext != null) { + TestSqlAnnotationHandler.handle(specDefinition, applicationContext, Sql.Phase.BEFORE_ALL); + } } @Override public void afterTestClass(TestContext testContext) throws Exception { fireListeners(TestExecutionListener::afterTestClass, testContext, true); + if (specDefinition != null && applicationContext != null) { + TestSqlAnnotationHandler.handle(specDefinition, applicationContext, Sql.Phase.AFTER_ALL); + } } @Override @@ -212,11 +220,17 @@ public void afterSetupTest(TestContext testContext) throws Exception { @Override public void beforeTestMethod(TestContext testContext) throws Exception { fireListeners(TestExecutionListener::beforeTestMethod, testContext, false); + if (specDefinition != null && applicationContext != null) { + TestSqlAnnotationHandler.handle(specDefinition, applicationContext, Sql.Phase.BEFORE_EACH); + } } @Override public void afterTestMethod(TestContext testContext) throws Exception { fireListeners(TestExecutionListener::afterTestMethod, testContext, true); + if (specDefinition != null && applicationContext != null) { + TestSqlAnnotationHandler.handle(specDefinition, applicationContext, Sql.Phase.AFTER_EACH); + } } /** diff --git a/test-core/src/main/java/io/micronaut/test/support/sql/ConnectionFactoryHandler.java b/test-core/src/main/java/io/micronaut/test/support/sql/ConnectionFactoryHandler.java new file mode 100644 index 000000000..1ad039ac2 --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/support/sql/ConnectionFactoryHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.support.sql; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Handler for r2dbc {@link ConnectionFactory} instances. + * + * @since 4.1.0 + * @author Tim Yates + */ +@Singleton +@Internal +@Experimental +@Requires(classes = {ConnectionFactory.class}) +public class ConnectionFactoryHandler implements SqlHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ConnectionFactoryHandler.class); + + @Override + public void handle(@NonNull ConnectionFactory connectionFactory, @NonNull String sql) { + List rowsUpdated = Mono.from(connectionFactory.create()) + .flatMapMany(c -> { + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Executing SQL: {}", connectionFactory, sql); + } + return c.createStatement(sql).execute(); + }) + .flatMap(Result::getRowsUpdated) + .collectList() + .block(); + + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Updated rows: {}", connectionFactory, rowsUpdated); + } + } +} diff --git a/test-core/src/main/java/io/micronaut/test/support/sql/DataSourceSqlHandler.java b/test-core/src/main/java/io/micronaut/test/support/sql/DataSourceSqlHandler.java new file mode 100644 index 000000000..1a690e2cd --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/support/sql/DataSourceSqlHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.support.sql; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.SQLException; + +/** + * Handler for raw {@link DataSource} instances. + * + * @since 4.1.0 + * @author Tim Yates + */ +@Singleton +@Internal +@Experimental +@Requires(classes = {DataSource.class}) +public class DataSourceSqlHandler implements SqlHandler { + + private static final Logger LOG = LoggerFactory.getLogger(DataSourceSqlHandler.class); + + @Override + public void handle(@NonNull DataSource dataSource, @NonNull String sql) { + try (var connection = dataSource.getConnection(); + var statement = connection.createStatement() + ) { + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Executing SQL: {}", dataSource, sql); + } + statement.execute(sql); + } catch (SQLException sqlException) { + throw new SqlAnnotationHandlingException(sqlException); + } + } +} diff --git a/test-core/src/main/java/io/micronaut/test/support/sql/DelegatingDataSourceSqlHandler.java b/test-core/src/main/java/io/micronaut/test/support/sql/DelegatingDataSourceSqlHandler.java new file mode 100644 index 000000000..f88a9649b --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/support/sql/DelegatingDataSourceSqlHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.support.sql; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; + +/** + * Handler for {@link DataSource} instances which may be a {@link DelegatingDataSource}. + * + * @since 4.1.0 + * @author Tim Yates + */ +@Singleton +@Internal +@Experimental +@Requires(classes = {DelegatingDataSource.class, DataSource.class}) +@Replaces(DataSourceSqlHandler.class) +public class DelegatingDataSourceSqlHandler extends DataSourceSqlHandler { + + private static final Logger LOG = LoggerFactory.getLogger(DelegatingDataSourceSqlHandler.class); + + @Override + public void handle(@NonNull DataSource dataSource, @NonNull String sql) { + if (dataSource instanceof DelegatingDataSource delegatingDataSource) { + if (LOG.isTraceEnabled()) { + LOG.trace("Unwrapping DelegatingDataSource: {}", delegatingDataSource); + } + dataSource = delegatingDataSource.getTargetDataSource(); + } + super.handle(dataSource, sql); + } +} diff --git a/test-core/src/main/java/io/micronaut/test/support/sql/SqlAnnotationHandlingException.java b/test-core/src/main/java/io/micronaut/test/support/sql/SqlAnnotationHandlingException.java new file mode 100644 index 000000000..2ed0287d4 --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/support/sql/SqlAnnotationHandlingException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.support.sql; + +import io.micronaut.test.annotation.Sql; + +/** + * Exception thrown when an error occurs handling an {@link Sql} annotation. + */ +public final class SqlAnnotationHandlingException extends RuntimeException { + + public SqlAnnotationHandlingException(Exception e) { + super(e); + } +} diff --git a/test-core/src/main/java/io/micronaut/test/support/sql/SqlHandler.java b/test-core/src/main/java/io/micronaut/test/support/sql/SqlHandler.java new file mode 100644 index 000000000..9419aa754 --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/support/sql/SqlHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.support.sql; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +/** + * Interface for handling Sql annotation for different data sources. + * + * @param The type of the data source + * + * @since 4.1.0 + * @author Tim Yates + */ +@Internal +@Experimental +@FunctionalInterface +public interface SqlHandler { + + /** + * Given a data source and SQL, execute the SQL. + * + * @param source The data source + * @param sql The SQL to execute + */ + void handle(@NonNull T source, @NonNull String sql); +} diff --git a/test-core/src/main/java/io/micronaut/test/support/sql/TestSqlAnnotationHandler.java b/test-core/src/main/java/io/micronaut/test/support/sql/TestSqlAnnotationHandler.java new file mode 100644 index 000000000..eadf5c818 --- /dev/null +++ b/test-core/src/main/java/io/micronaut/test/support/sql/TestSqlAnnotationHandler.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.test.support.sql; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.io.ResourceLoader; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.test.annotation.Sql; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * Static helper class to handle {@link Sql} annotations. + * + * @since 4.1.0 + * @author Tim Yates + */ +@Internal +@Experimental +public final class TestSqlAnnotationHandler { + + private static final Logger LOG = LoggerFactory.getLogger(TestSqlAnnotationHandler.class); + + private TestSqlAnnotationHandler() { + } + + /** + * Given a spec definition and application context, find and process all {@link Sql} annotations. + * + * @param specDefinition The test class + * @param applicationContext The application context + * @param phase The {@link Sql.Phase} to run the scripts in + * + * @throws SQLException If an error occurs executing the SQL + * @throws IOException If an error occurs reading the SQL + */ + public static void handle(BeanDefinition specDefinition, ApplicationContext applicationContext, Sql.Phase phase) throws IOException { + ResourceLoader resourceLoader = applicationContext.getBean(ResourceLoader.class); + Optional>> sqlAnnotations = specDefinition + .findAnnotation(Sql.Sqls.class) + .map(s -> s.getAnnotations("value", Sql.class)); + + if (sqlAnnotations.isPresent()) { + for (var sql : sqlAnnotations.get()) { + if (sql.getRequiredValue("phase", Sql.Phase.class) != phase) { + continue; + } + List<@NonNull String> scripts = Arrays.asList(sql.stringValues()); + if (!scripts.isEmpty()) { + Consumer proc = bean( + sql.getRequiredValue("resourceType", Class.class), + sql.getRequiredValue("dataSourceName", String.class), + applicationContext + ); + handleScript(resourceLoader, scripts, proc, phase); + } else if (LOG.isTraceEnabled()) { + LOG.trace("No SQL scripts found for {} phase", phase); + } + } + } + } + + private static Consumer bean(Class dataSourceType, String dataSourceName, ApplicationContext applicationContext) { + T ds = applicationContext.getBean(dataSourceType, Qualifiers.byName(dataSourceName)); + SqlHandler handler = applicationContext.getBean(SqlHandler.class, Qualifiers.byTypeArguments(dataSourceType)); + + return (String s) -> handler.handle(ds, s); + } + + private static void handleScript(ResourceLoader loader, List scripts, Consumer processor, Sql.Phase phase) throws IOException { + for (String script : scripts) { + Optional resource = loader.getResource(script); + if (resource.isPresent()) { + if (LOG.isTraceEnabled()) { + LOG.trace("Processing {} SQL script: {}", phase, script); + } + try (InputStream in = resource.get().openStream()) { + String scriptBody = new String(in.readAllBytes(), StandardCharsets.UTF_8); + processor.accept(scriptBody); + } + } else { + LOG.warn("Could not find SQL script: {}", script); + } + } + } + +} diff --git a/test-junit5/build.gradle b/test-junit5/build.gradle index 73bccd86e..9b9e1b02b 100644 --- a/test-junit5/build.gradle +++ b/test-junit5/build.gradle @@ -54,6 +54,7 @@ dependencies { testImplementation(mnData.micronaut.data.hibernate.jpa) testImplementation(mn.snakeyaml) + testRuntimeOnly(mnLogging.logback.classic) testRuntimeOnly(mnSql.micronaut.jdbc.tomcat) testRuntimeOnly(mnSql.h2) testRuntimeOnly(mnSerde.micronaut.serde.jackson) diff --git a/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java b/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java index 8793474a3..5bc01f7de 100644 --- a/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java +++ b/test-junit5/src/main/java/io/micronaut/test/extensions/junit5/MicronautJunit5Extension.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test-junit5/src/test/java/io/micronaut/test/junit5/SqlDatasourceTest.java b/test-junit5/src/test/java/io/micronaut/test/junit5/SqlDatasourceTest.java new file mode 100644 index 000000000..4d84bfdbe --- /dev/null +++ b/test-junit5/src/test/java/io/micronaut/test/junit5/SqlDatasourceTest.java @@ -0,0 +1,54 @@ +package io.micronaut.test.junit5; + +import io.micronaut.context.annotation.Property; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.annotation.TransactionMode; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DbProperties +// tag::clazz[] +@MicronautTest(transactionMode = TransactionMode.SINGLE_TRANSACTION) +@Property(name = "datasources.default.dialect", value = "H2") +@Property(name = "datasources.default.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.default.username", value = "sa") + +@Sql({"classpath:create.sql", "classpath:datasource_1_insert.sql"}) // <1> +class SqlDatasourceTest { + + @Inject + DataSource dataSource; + + @Test + void dataIsInserted() throws Exception { + assertEquals(List.of("Aardvark", "Albatross"), readAllNames(dataSource)); + } + + List readAllNames(DataSource dataSource) throws SQLException { + var result = new ArrayList(); + try ( + Connection ds = dataSource.getConnection(); + PreparedStatement ps = ds.prepareStatement("select name from MyTable"); + ResultSet rslt = ps.executeQuery() + ) { + while(rslt.next()) { + result.add(rslt.getString(1)); + } + } + return result; + } +} +// end::clazz[] diff --git a/test-junit5/src/test/java/io/micronaut/test/junit5/SqlNamedDatasourceTest.java b/test-junit5/src/test/java/io/micronaut/test/junit5/SqlNamedDatasourceTest.java new file mode 100644 index 000000000..e4d34948c --- /dev/null +++ b/test-junit5/src/test/java/io/micronaut/test/junit5/SqlNamedDatasourceTest.java @@ -0,0 +1,69 @@ +package io.micronaut.test.junit5; + +import io.micronaut.context.annotation.Property; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.annotation.TransactionMode; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DbProperties + +// tag::clazz[] +@MicronautTest + +@Sql(dataSourceName = "one", value = {"classpath:create.sql", "classpath:datasource_1_insert.sql"}) // <1> +@Property(name = "datasources.one.dialect", value = "H2") +@Property(name = "datasources.one.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.one.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.one.url", value = "jdbc:h2:mem:databaseOne;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.one.username", value = "sa") + +@Sql(dataSourceName = "two", scripts = {"classpath:create.sql", "classpath:datasource_2_insert.sql"}) // <1> +@Property(name = "datasources.two.dialect", value = "H2") +@Property(name = "datasources.two.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.two.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.two.url", value = "jdbc:h2:mem:databaseTwo;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.two.username", value = "sa") +class SqlNamedDatasourceTest { + + @Inject + @Named("one") + DataSource dataSource1; + + @Inject + @Named("two") + DataSource dataSource2; + + @Test + void dataIsInserted() throws Exception { + assertEquals(List.of("Aardvark", "Albatross"), readAllNames(dataSource1)); + assertEquals(List.of("Bear", "Bumblebee"), readAllNames(dataSource2)); + } + + List readAllNames(DataSource dataSource) throws SQLException { + var result = new ArrayList(); + try ( + Connection ds = dataSource.getConnection(); + PreparedStatement ps = ds.prepareStatement("select name from MyTable"); + ResultSet rslt = ps.executeQuery() + ) { + while(rslt.next()) { + result.add(rslt.getString(1)); + } + } + return result; + } +} +// end::clazz[] diff --git a/test-junit5/src/test/java/io/micronaut/test/junit5/server/ExternalServerTest.java b/test-junit5/src/test/java/io/micronaut/test/junit5/server/ExternalServerTest.java index 23262a507..1c338f969 100644 --- a/test-junit5/src/test/java/io/micronaut/test/junit5/server/ExternalServerTest.java +++ b/test-junit5/src/test/java/io/micronaut/test/junit5/server/ExternalServerTest.java @@ -21,7 +21,7 @@ @MicronautTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class ExternalServerTest implements TestPropertyProvider { +class ExternalServerTest implements TestPropertyProvider { static EmbeddedServer EXTERNAL_SERVER = ApplicationContext.run( EmbeddedServer.class, diff --git a/test-junit5/src/test/resources/create.sql b/test-junit5/src/test/resources/create.sql new file mode 100644 index 000000000..5f3d53bf7 --- /dev/null +++ b/test-junit5/src/test/resources/create.sql @@ -0,0 +1,5 @@ +CREATE TABLE MyTable ( + ID INT NOT NULL, + NAME VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/test-junit5/src/test/resources/datasource_1_insert.sql b/test-junit5/src/test/resources/datasource_1_insert.sql new file mode 100644 index 000000000..aceec1404 --- /dev/null +++ b/test-junit5/src/test/resources/datasource_1_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Aardvark'); +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Albatross'); diff --git a/test-junit5/src/test/resources/datasource_2_insert.sql b/test-junit5/src/test/resources/datasource_2_insert.sql new file mode 100644 index 000000000..f1121a5ac --- /dev/null +++ b/test-junit5/src/test/resources/datasource_2_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Bear'); +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Bumblebee'); diff --git a/test-junit5/src/test/resources/logback.xml b/test-junit5/src/test/resources/logback.xml index a0d26b4c4..f75b890dd 100644 --- a/test-junit5/src/test/resources/logback.xml +++ b/test-junit5/src/test/resources/logback.xml @@ -13,4 +13,5 @@ - \ No newline at end of file + + diff --git a/test-kotest5/build.gradle b/test-kotest5/build.gradle index ef2522f60..d9e101a37 100644 --- a/test-kotest5/build.gradle +++ b/test-kotest5/build.gradle @@ -30,6 +30,7 @@ dependencies { testImplementation(mnData.micronaut.data.tx) testImplementation(mnData.micronaut.data.hibernate.jpa) + testRuntimeOnly(mnLogging.logback.classic) testRuntimeOnly(mnSql.micronaut.jdbc.tomcat) testRuntimeOnly(mnSql.h2) testRuntimeOnly(mnSerde.micronaut.serde.jackson) diff --git a/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt b/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt index 746e22437..cb11ecbde 100644 --- a/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt +++ b/test-kotest5/src/main/kotlin/io/micronaut/test/extensions/kotest5/MicronautKotest5Context.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/SqlDatasourceTest.kt b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/SqlDatasourceTest.kt new file mode 100644 index 000000000..232af14c3 --- /dev/null +++ b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/SqlDatasourceTest.kt @@ -0,0 +1,44 @@ +package io.micronaut.test.kotest5 + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.annotation.Property +import io.micronaut.test.annotation.Sql +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest +import javax.sql.DataSource + +@DbProperties +// tag::clazz[] +@MicronautTest +@Property(name = "datasources.default.dialect", value = "H2") +@Property(name = "datasources.default.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.default.username", value = "sa") + +@Sql("classpath:create.sql", "classpath:datasource_1_insert.sql") // <1> +class SqlDatasourceTest( + private val dataSource: DataSource +): BehaviorSpec({ + + fun readAllNames(dataSource: DataSource): List { + val result = mutableListOf() + dataSource.connection.use { ds -> + ds.prepareStatement("select name from MyTable").use { ps -> + ps.executeQuery().use { rslt -> + while (rslt.next()) { + result.add(rslt.getString(1)) + } + } + } + } + return result + } + + given("a test with the Sql annotation") { + then("the data is inserted as expected") { + readAllNames(dataSource) shouldBe listOf("Aardvark", "Albatross") + } + } +}) +// end::clazz[] diff --git a/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/SqlNamedDatasourceTest.kt b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/SqlNamedDatasourceTest.kt new file mode 100644 index 000000000..68607a3ab --- /dev/null +++ b/test-kotest5/src/test/kotlin/io/micronaut/test/kotest5/SqlNamedDatasourceTest.kt @@ -0,0 +1,55 @@ +package io.micronaut.test.kotest5 + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.annotation.Property +import io.micronaut.test.annotation.Sql +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest +import jakarta.inject.Named +import javax.sql.DataSource + +@DbProperties + +// tag::clazz[] +@MicronautTest +@Sql(dataSourceName = "one", value = ["classpath:create.sql", "classpath:datasource_1_insert.sql"]) // <1> +@Property(name = "datasources.one.dialect", value = "H2") +@Property(name = "datasources.one.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.one.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.one.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.one.username", value = "sa") + +@Sql(dataSourceName = "two", value = ["classpath:create.sql", "classpath:datasource_2_insert.sql"]) // <1> +@Property(name = "datasources.two.dialect", value = "H2") +@Property(name = "datasources.two.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.two.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.two.url", value = "jdbc:h2:mem:devDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.two.username", value = "sa") +class SqlNamedDatasourceTest( + @Named("one") private val dataSource1: DataSource, + @Named("two") private val dataSource2: DataSource +): BehaviorSpec({ + + fun readAllNames(dataSource: DataSource): List { + val result = mutableListOf() + println("Testing datasource: $dataSource") + dataSource.connection.use { ds -> + ds.prepareStatement("select name from MyTable").use { ps -> + ps.executeQuery().use { rslt -> + while (rslt.next()) { + result.add(rslt.getString(1)) + } + } + } + } + return result + } + + given("a test with the Sql annotation") { + then("the data is inserted as expected") { + readAllNames(dataSource1) shouldBe listOf("Aardvark", "Albatross") + readAllNames(dataSource2) shouldBe listOf("Bear", "Bumblebee") + } + } +}) +// end::clazz[] diff --git a/test-kotest5/src/test/resources/create.sql b/test-kotest5/src/test/resources/create.sql new file mode 100644 index 000000000..5f3d53bf7 --- /dev/null +++ b/test-kotest5/src/test/resources/create.sql @@ -0,0 +1,5 @@ +CREATE TABLE MyTable ( + ID INT NOT NULL, + NAME VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/test-kotest5/src/test/resources/datasource_1_insert.sql b/test-kotest5/src/test/resources/datasource_1_insert.sql new file mode 100644 index 000000000..aceec1404 --- /dev/null +++ b/test-kotest5/src/test/resources/datasource_1_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Aardvark'); +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Albatross'); diff --git a/test-kotest5/src/test/resources/datasource_2_insert.sql b/test-kotest5/src/test/resources/datasource_2_insert.sql new file mode 100644 index 000000000..f1121a5ac --- /dev/null +++ b/test-kotest5/src/test/resources/datasource_2_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Bear'); +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Bumblebee'); diff --git a/test-kotest5/src/test/resources/logback.xml b/test-kotest5/src/test/resources/logback.xml index a0d26b4c4..f75b890dd 100644 --- a/test-kotest5/src/test/resources/logback.xml +++ b/test-kotest5/src/test/resources/logback.xml @@ -13,4 +13,5 @@ - \ No newline at end of file + + diff --git a/test-spock/build.gradle b/test-spock/build.gradle index 46b73e41f..cefadcc21 100644 --- a/test-spock/build.gradle +++ b/test-spock/build.gradle @@ -24,6 +24,7 @@ dependencies { testImplementation(mn.snakeyaml) testImplementation(mnReactor.micronaut.reactor) + testRuntimeOnly(mnLogging.logback.classic) testRuntimeOnly(mnSerde.micronaut.serde.jackson) testRuntimeOnly(mnSql.h2) testRuntimeOnly(testFixtures(projects.micronautTestCore)) diff --git a/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java b/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java index 96803aa0b..c86f1f532 100644 --- a/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java +++ b/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test-spock/src/test/groovy/io/micronaut/test/spock/SqlDatasourceSpec.groovy b/test-spock/src/test/groovy/io/micronaut/test/spock/SqlDatasourceSpec.groovy new file mode 100644 index 000000000..2062b8b49 --- /dev/null +++ b/test-spock/src/test/groovy/io/micronaut/test/spock/SqlDatasourceSpec.groovy @@ -0,0 +1,44 @@ +package io.micronaut.test.spock + +import io.micronaut.context.annotation.Property +import io.micronaut.test.annotation.Sql +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +import javax.sql.DataSource + +// tag::clazz[] +@MicronautTest +@Property(name = "datasources.default.dialect", value = "H2") +@Property(name = "datasources.default.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.default.username", value = "sa") + +@Sql(["classpath:create.sql", "classpath:datasource_1_insert.sql"]) // <1> +class SqlDatasourceSpec extends Specification { + + @Inject + DataSource dataSource + + def "data is inserted"() { + expect: + readAllNames(dataSource) == ["Aardvark", "Albatross"] + } + + List readAllNames(DataSource dataSource) { + dataSource.getConnection().withCloseable { + it.prepareStatement("select name from MyTable").withCloseable { + it.executeQuery().withCloseable { + def names = [] + while (it.next()) { + names << it.getString(1) + } + names + } + } + } + } +} +// end::clazz[] diff --git a/test-spock/src/test/groovy/io/micronaut/test/spock/SqlNamedDatasourceSpec.groovy b/test-spock/src/test/groovy/io/micronaut/test/spock/SqlNamedDatasourceSpec.groovy new file mode 100644 index 000000000..e7888d332 --- /dev/null +++ b/test-spock/src/test/groovy/io/micronaut/test/spock/SqlNamedDatasourceSpec.groovy @@ -0,0 +1,60 @@ +package io.micronaut.test.spock + +import io.micronaut.context.annotation.Property +import io.micronaut.test.annotation.Sql +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.inject.Named +import spock.lang.Specification + +import javax.sql.DataSource + + +// tag::clazz[] +@MicronautTest +@Sql(dataSourceName = "one", value = ["classpath:create.sql", "classpath:datasource_1_insert.sql"]) // <1> +@Property(name = "datasources.one.dialect", value = "H2") +@Property(name = "datasources.one.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.one.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.one.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.one.username", value = "sa") + +@Sql(dataSourceName = "two", value = ["classpath:create.sql", "classpath:datasource_2_insert.sql"]) // <1> +@Property(name = "datasources.two.dialect", value = "H2") +@Property(name = "datasources.two.driverClassName", value = "org.h2.Driver") +@Property(name = "datasources.two.schema-generate", value = "CREATE_DROP") +@Property(name = "datasources.two.url", value = "jdbc:h2:mem:devDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") +@Property(name = "datasources.two.username", value = "sa") +class SqlNamedDatasourceSpec extends Specification { + + @Inject + @Named("one") + DataSource dataSource1 + + @Inject + @Named("two") + DataSource dataSource2 + + def "data is inserted"() { + expect: + readAllNames(dataSource1) == ["Aardvark", "Albatross"] + + and: + readAllNames(dataSource2) == ["Bear", "Bumblebee"] + } + + List readAllNames(DataSource dataSource) { + dataSource.getConnection().withCloseable { + it.prepareStatement("select name from MyTable").withCloseable { + it.executeQuery().withCloseable { + def names = [] + while (it.next()) { + names << it.getString(1) + } + names + } + } + } + } +} +// end::clazz[] diff --git a/test-spock/src/test/resources/create.sql b/test-spock/src/test/resources/create.sql new file mode 100644 index 000000000..5f3d53bf7 --- /dev/null +++ b/test-spock/src/test/resources/create.sql @@ -0,0 +1,5 @@ +CREATE TABLE MyTable ( + ID INT NOT NULL, + NAME VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/test-spock/src/test/resources/datasource_1_insert.sql b/test-spock/src/test/resources/datasource_1_insert.sql new file mode 100644 index 000000000..aceec1404 --- /dev/null +++ b/test-spock/src/test/resources/datasource_1_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Aardvark'); +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Albatross'); diff --git a/test-spock/src/test/resources/datasource_2_insert.sql b/test-spock/src/test/resources/datasource_2_insert.sql new file mode 100644 index 000000000..f1121a5ac --- /dev/null +++ b/test-spock/src/test/resources/datasource_2_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Bear'); +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Bumblebee'); diff --git a/test-spock/src/test/resources/logback.xml b/test-spock/src/test/resources/logback.xml index 98cd8e0ca..d11b56fa3 100644 --- a/test-spock/src/test/resources/logback.xml +++ b/test-spock/src/test/resources/logback.xml @@ -12,5 +12,6 @@ - + + diff --git a/test-suite-at-sql-jpa/build.gradle.kts b/test-suite-at-sql-jpa/build.gradle.kts new file mode 100644 index 000000000..8aee0e533 --- /dev/null +++ b/test-suite-at-sql-jpa/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + `java-library` +} + +repositories { + mavenCentral() +} + +dependencies { + annotationProcessor(mn.micronaut.inject.java) + testAnnotationProcessor(mn.micronaut.inject.java) + + annotationProcessor(mnData.micronaut.data.processor) + implementation(mnData.micronaut.data.hibernate.jpa) + implementation(mnSql.micronaut.jdbc.hikari) + runtimeOnly(mnSql.postgresql) + + runtimeOnly(mnLogging.logback.classic) + + testImplementation(libs.junit.jupiter.api) + testImplementation(projects.micronautTestJunit5) + testRuntimeOnly(libs.junit.jupiter.engine) + + testImplementation(platform(libs.testcontainers.bom)) + testImplementation(libs.testcontainers) + testImplementation(libs.testcontainers.postgresql) +} +tasks.withType { + useJUnitPlatform() +} diff --git a/test-suite-at-sql-jpa/src/main/java/example/micronaut/ProductRepository.java b/test-suite-at-sql-jpa/src/main/java/example/micronaut/ProductRepository.java new file mode 100644 index 000000000..9c659db6b --- /dev/null +++ b/test-suite-at-sql-jpa/src/main/java/example/micronaut/ProductRepository.java @@ -0,0 +1,9 @@ +package example.micronaut; + +import example.micronaut.entities.Product; +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.jpa.repository.JpaRepository; + +@Repository +interface ProductRepository extends JpaRepository { +} diff --git a/test-suite-at-sql-jpa/src/main/java/example/micronaut/entities/Product.java b/test-suite-at-sql-jpa/src/main/java/example/micronaut/entities/Product.java new file mode 100644 index 000000000..7fcffa5b2 --- /dev/null +++ b/test-suite-at-sql-jpa/src/main/java/example/micronaut/entities/Product.java @@ -0,0 +1,52 @@ +package example.micronaut.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "products") +public class Product { + + @Id + private Long id; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private String name; + + public Product() {} + + public Product(Long id, String code, String name) { + this.id = id; + this.code = code; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/test-suite-at-sql-jpa/src/main/resources/logback.xml b/test-suite-at-sql-jpa/src/main/resources/logback.xml new file mode 100644 index 000000000..f75b890dd --- /dev/null +++ b/test-suite-at-sql-jpa/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/test-suite-at-sql-jpa/src/test/java/example/micronaut/PostgreSQL.java b/test-suite-at-sql-jpa/src/test/java/example/micronaut/PostgreSQL.java new file mode 100644 index 000000000..ce6b8ba10 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/java/example/micronaut/PostgreSQL.java @@ -0,0 +1,28 @@ +package example.micronaut; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.support.TestPropertyProvider; +import org.testcontainers.containers.PostgreSQLContainer; + +import java.util.Map; + +public class PostgreSQL { + static PostgreSQLContainer postgres = new PostgreSQLContainer<>( + "postgres:15.2-alpine" + ); + + public static @NonNull Map getProperties() { + if (!postgres.isRunning()) { + postgres.start(); + } + return Map.of( + "jpa.default.entity-scan.packages", "example.micronaut.entities", + "jpa.default.properties.hibernate.hbm2ddl.auto", "update", + "datasources.default.db-type", "postgres", + "datasources.default.dialect", "POSTGRES", + "datasources.default.driver-class-name", "org.postgresql.Driver", + "datasources.default.url", postgres.getJdbcUrl(), + "datasources.default.username", postgres.getUsername(), + "datasources.default.password", postgres.getPassword()); + } +} diff --git a/test-suite-at-sql-jpa/src/test/java/example/micronaut/ThreeProductsTest.java b/test-suite-at-sql-jpa/src/test/java/example/micronaut/ThreeProductsTest.java new file mode 100644 index 000000000..8f110207a --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/java/example/micronaut/ThreeProductsTest.java @@ -0,0 +1,31 @@ +package example.micronaut; + +import example.micronaut.entities.Product; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Sql(scripts = "classpath:threeproducts.sql") +@Sql(scripts = "classpath:rollbackthreeproducts.sql", phase = Sql.Phase.AFTER_ALL) +@MicronautTest(startApplication = false) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ThreeProductsTest implements TestPropertyProvider { + + @Override + public @NonNull Map getProperties() { + return PostgreSQL.getProperties(); + } + + @Test + void thereAreTwoProducts(ProductRepository productRepository) { + assertEquals(3L, productRepository.count()); + productRepository.save(new Product(999L, "foo", "bar")); + } +} diff --git a/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsEachTest.java b/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsEachTest.java new file mode 100644 index 000000000..cffd88b10 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsEachTest.java @@ -0,0 +1,37 @@ +package example.micronaut; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Sql(scripts = "classpath:tworandomproducts.sql", phase = Sql.Phase.BEFORE_EACH) +@Sql(scripts = "classpath:rollbackallproducts.sql", phase = Sql.Phase.AFTER_ALL) +@MicronautTest(startApplication = false) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TwoProductsEachTest implements TestPropertyProvider { + + @Override + public @NonNull Map getProperties() { + return PostgreSQL.getProperties(); + } + + @Test + @Order(1) + void thereAreTwoProducts(ProductRepository productRepository) { + assertEquals(2L, productRepository.count()); + } + + @Test + @Order(2) + void thereAreThenFourProducts(ProductRepository productRepository) { + assertEquals(4L, productRepository.count()); + } +} diff --git a/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsTest.java b/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsTest.java new file mode 100644 index 000000000..d442d7c12 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsTest.java @@ -0,0 +1,29 @@ +package example.micronaut; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Sql(scripts = "classpath:twoproducts.sql") +@Sql(scripts = "classpath:rollbacktwoproducts.sql", phase = Sql.Phase.AFTER_ALL) +@MicronautTest(startApplication = false) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TwoProductsTest implements TestPropertyProvider { + + @Override + public @NonNull Map getProperties() { + return PostgreSQL.getProperties(); + } + + @Test + void thereAreTwoProducts(ProductRepository productRepository) { + assertEquals(2L, productRepository.count()); + } +} diff --git a/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsThenNoneTest.java b/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsThenNoneTest.java new file mode 100644 index 000000000..613ec8f60 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/java/example/micronaut/TwoProductsThenNoneTest.java @@ -0,0 +1,39 @@ +package example.micronaut; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Sql(scripts = "classpath:twoproducts.sql") +// tag::rollback[] +@Sql(scripts = "classpath:rollbacktwoproducts.sql", phase = Sql.Phase.AFTER_EACH) // <1> +// end::rollback[] +@MicronautTest(startApplication = false) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TwoProductsThenNoneTest implements TestPropertyProvider { + + @Override + public @NonNull Map getProperties() { + return PostgreSQL.getProperties(); + } + + @Test + @Order(1) + void thereAreTwoProducts(ProductRepository productRepository) { + assertEquals(2L, productRepository.count()); + } + + @Test + @Order(2) + void thereAreThenZeroProducts(ProductRepository productRepository) { + assertEquals(0L, productRepository.count()); + } +} diff --git a/test-suite-at-sql-jpa/src/test/resources/rollbackallproducts.sql b/test-suite-at-sql-jpa/src/test/resources/rollbackallproducts.sql new file mode 100644 index 000000000..d506b042f --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/resources/rollbackallproducts.sql @@ -0,0 +1 @@ +TRUNCATE TABLE products; diff --git a/test-suite-at-sql-jpa/src/test/resources/rollbackthreeproducts.sql b/test-suite-at-sql-jpa/src/test/resources/rollbackthreeproducts.sql new file mode 100644 index 000000000..0d92bbdd3 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/resources/rollbackthreeproducts.sql @@ -0,0 +1,3 @@ +DELETE FROM products WHERE ID = 1; +DELETE FROM products WHERE ID = 2; +DELETE FROM products WHERE ID = 3; diff --git a/test-suite-at-sql-jpa/src/test/resources/rollbacktwoproducts.sql b/test-suite-at-sql-jpa/src/test/resources/rollbacktwoproducts.sql new file mode 100644 index 000000000..8630578e7 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/resources/rollbacktwoproducts.sql @@ -0,0 +1,2 @@ +DELETE FROM products WHERE ID = 1; +DELETE FROM products WHERE ID = 2; diff --git a/test-suite-at-sql-jpa/src/test/resources/threeproducts.sql b/test-suite-at-sql-jpa/src/test/resources/threeproducts.sql new file mode 100644 index 000000000..b852964a7 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/resources/threeproducts.sql @@ -0,0 +1,3 @@ +insert into products(id, code, name) values(1, 'p101', 'Apple MacBook Pro') ON CONFLICT DO NOTHING; +insert into products(id, code, name) values(2, 'p102', 'Sony TV') ON CONFLICT DO NOTHING; +insert into products(id, code, name) values(3, 'p103', 'Lenovo TV') ON CONFLICT DO NOTHING; diff --git a/test-suite-at-sql-jpa/src/test/resources/twoproducts.sql b/test-suite-at-sql-jpa/src/test/resources/twoproducts.sql new file mode 100644 index 000000000..c537b0a68 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/resources/twoproducts.sql @@ -0,0 +1,2 @@ +insert into products(id, code, name) values(1, 'p101', 'Apple MacBook Pro') ON CONFLICT DO NOTHING; +insert into products(id, code, name) values(2, 'p102', 'Sony TV') ON CONFLICT DO NOTHING; diff --git a/test-suite-at-sql-jpa/src/test/resources/tworandomproducts.sql b/test-suite-at-sql-jpa/src/test/resources/tworandomproducts.sql new file mode 100644 index 000000000..b6b7b4c52 --- /dev/null +++ b/test-suite-at-sql-jpa/src/test/resources/tworandomproducts.sql @@ -0,0 +1,3 @@ +-- Try to add 2 products with random id's so they don't conflict with the previous 2 products. +insert into products(id, code, name) values(floor(random() * 1000000000), 'p103', 'Apple Studio') ON CONFLICT DO NOTHING; +insert into products(id, code, name) values(floor(random() * 1000000000), 'p104', 'Samsung TV') ON CONFLICT DO NOTHING; diff --git a/test-suite-sql-r2dbc/build.gradle.kts b/test-suite-sql-r2dbc/build.gradle.kts new file mode 100644 index 000000000..f23cc6724 --- /dev/null +++ b/test-suite-sql-r2dbc/build.gradle.kts @@ -0,0 +1,36 @@ +import io.micronaut.testresources.buildtools.KnownModules.R2DBC_MYSQL + +plugins { + id("io.micronaut.library") + id("io.micronaut.test-resources") + id("io.micronaut.graalvm") // Required to configure Graal for nativeTest +} + +repositories { + mavenCentral() +} + +tasks.withType(Test::class).configureEach { + useJUnitPlatform() +} + +dependencies { + testAnnotationProcessor(mnData.micronaut.data.processor) + testAnnotationProcessor(mnSerde.micronaut.serde.processor) + + testImplementation(projects.micronautTestJunit5) + testImplementation(mnData.micronaut.data.r2dbc) + testImplementation(mnSerde.micronaut.serde.jackson) + + testRuntimeOnly(mnLogging.logback.classic) + testRuntimeOnly(mnR2dbc.r2dbc.mysql) + + testResourcesService(mnSql.mysql.connector.java) +} + +micronaut { + importMicronautPlatform.set(false) + testResources { + additionalModules.add(R2DBC_MYSQL) + } +} diff --git a/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/MySqlConnectionTest.java b/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/MySqlConnectionTest.java new file mode 100644 index 000000000..4afce0026 --- /dev/null +++ b/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/MySqlConnectionTest.java @@ -0,0 +1,35 @@ +package io.micronaut.test.r2dbc; + +import io.micronaut.context.annotation.Property; +import io.micronaut.test.annotation.Sql; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.r2dbc.spi.ConnectionFactory; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// tag::clazz[] +@MicronautTest +@Property(name = "r2dbc.datasources.default.db-type", value = "mysql") +@Sql(value = {"classpath:create.sql", "classpath:datasource_1_insert.sql"}, resourceType = ConnectionFactory.class) +class MySqlConnectionTest { + + @Inject + ConnectionFactory connectionFactory; + + @Test + void testSqlHasBeenInjected() { + var f = Flux.from(connectionFactory.create()); + + var result = f.flatMap(connection -> + connection.createStatement("SELECT name from MyTable where id = 2").execute() + ).flatMap(rslt -> + rslt.map((row, metadata) -> row.get(0, String.class)) + ).blockFirst(); + + assertEquals("Albatross", result); + } +} +// end::clazz[] diff --git a/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/Pet.java b/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/Pet.java new file mode 100644 index 000000000..496cd904b --- /dev/null +++ b/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/Pet.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original 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 io.micronaut.test.r2dbc; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; + +@MappedEntity +public class Pet { + + @Id + @GeneratedValue(GeneratedValue.Type.IDENTITY) + private Long id; + + private String name; + + @Override + public String toString() { + return "Pet{" + + "id=" + id + + ", name='" + name + + '}'; + } +} diff --git a/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/PetRepository.java b/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/PetRepository.java new file mode 100644 index 000000000..b69febeec --- /dev/null +++ b/test-suite-sql-r2dbc/src/test/java/io/micronaut/test/r2dbc/PetRepository.java @@ -0,0 +1,13 @@ +package io.micronaut.test.r2dbc; + +import io.micronaut.data.repository.reactive.ReactorCrudRepository; +import io.micronaut.transaction.TransactionDefinition; +import io.micronaut.transaction.annotation.Transactional; +import io.r2dbc.spi.Connection; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Transactional(isolation = TransactionDefinition.Isolation.SERIALIZABLE) +public interface PetRepository extends ReactorCrudRepository { + Flux list(); +} diff --git a/test-suite-sql-r2dbc/src/test/resources/create.sql b/test-suite-sql-r2dbc/src/test/resources/create.sql new file mode 100644 index 000000000..ae09edd35 --- /dev/null +++ b/test-suite-sql-r2dbc/src/test/resources/create.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS MyTable ( + ID INT NOT NULL, + NAME VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/test-suite-sql-r2dbc/src/test/resources/datasource_1_insert.sql b/test-suite-sql-r2dbc/src/test/resources/datasource_1_insert.sql new file mode 100644 index 000000000..655532555 --- /dev/null +++ b/test-suite-sql-r2dbc/src/test/resources/datasource_1_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO MyTable (ID, NAME) VALUES (1, 'Aardvark') ON DUPLICATE KEY UPDATE NAME = NAME; +INSERT INTO MyTable (ID, NAME) VALUES (2, 'Albatross') ON DUPLICATE KEY UPDATE NAME = NAME; diff --git a/test-suite-sql-r2dbc/src/test/resources/logback.xml b/test-suite-sql-r2dbc/src/test/resources/logback.xml new file mode 100644 index 000000000..f75b890dd --- /dev/null +++ b/test-suite-sql-r2dbc/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + +