Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotation to load SQL before a test @Sql #851

Merged
merged 26 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c0dab7d
Add Spock support
timyates Sep 18, 2023
c48f5cc
Revert whitespace change
timyates Sep 18, 2023
359d59a
Remove anotation alias. Not sure how it works
timyates Sep 18, 2023
4b52572
Add JUnit and JPA support
timyates Sep 18, 2023
08c5813
Add kotest
timyates Sep 18, 2023
29e6f72
Documentation
timyates Sep 18, 2023
857a141
Update gradle/libs.versions.toml
sdelamo Sep 19, 2023
b3df3fa
Switch to use specDefinition and move code up out of the individual i…
timyates Sep 19, 2023
54c7000
Unused imports
timyates Sep 19, 2023
94c1994
Support R2DBC and add Graal test for it
timyates Sep 20, 2023
7026186
Add javadoc and remove un-required value finagling
timyates Sep 20, 2023
d167f5e
Unused import
timyates Sep 20, 2023
2807e8f
Move Sql invocation to fall inside transactions
timyates Sep 20, 2023
6a34932
Mark things as experimental
timyates Sep 20, 2023
d9ec210
Undo irrelevant changes
timyates Sep 20, 2023
f6fef78
Add internal
timyates Sep 21, 2023
93ce4c5
Switch versions around
timyates Sep 21, 2023
785a04f
Switch to just using generic bean loading
timyates Sep 21, 2023
97b886f
Address feedback
timyates Sep 21, 2023
07ac19c
dont import instead of set version for test-suite
timyates Sep 21, 2023
6870abc
Documentation
timyates Sep 21, 2023
800356e
add nullability annotations
sdelamo Sep 21, 2023
10cee0a
Merge branch 'master' into sql-annotation
sdelamo Oct 13, 2023
e151975
Sql annotation test project (#867)
sdelamo Oct 20, 2023
d26edd3
Merge branch 'master' into sql-annotation
timyates Oct 20, 2023
4aa68ff
Sonar smells
timyates Oct 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
repositories {
mavenCentral()
}
9 changes: 7 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[versions]
micronaut = "4.1.1"
timyates marked this conversation as resolved.
Show resolved Hide resolved
micronaut-core = "4.1.5"
micronaut-docs = "2.0.0"
micronaut = "4.1.5"

groovy = "4.0.11"

Expand All @@ -18,6 +19,8 @@ graal-svm = "23.0.1"

micronaut-data = "4.1.2"
micronaut-hibernate-validator = "4.0.2"
micronaut-logging = "1.1.2"
micronaut-r2dbc = "5.0.1"
micronaut-serde = "2.2.4"
micronaut-spring = "5.0.2"
micronaut-sql = "5.0.1"
Expand All @@ -27,10 +30,12 @@ micronaut-gradle-plugin = "4.1.0"

[libraries]
# Core
micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' }
micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut-core' }
micronaut-platform = { module = 'io.micronaut.platform:micronaut-platform', version.ref = 'micronaut' }

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" }
Expand Down
3 changes: 3 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ micronautBuild {
importMicronautCatalog("micronaut-spring")
importMicronautCatalog("micronaut-sql")
importMicronautCatalog("micronaut-reactor")
importMicronautCatalog("micronaut-r2dbc")
}

rootProject.name = 'test-parent'
Expand All @@ -29,3 +30,5 @@ include "test-spock"
include "test-junit5"
include "test-kotest5"
include "test-rest-assured"

include 'test-suite-sql-r2dbc'
41 changes: 41 additions & 0 deletions src/main/docs/guide/sql.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
:icons: font
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 prior to the test.
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> The annotation is used to specify the location of the SQL scripts to be executed. (The default datasource name is `default`, but this can be overriden by setting the `datasourceName` property for the annotation)
1 change: 1 addition & 0 deletions src/main/docs/guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ spock: Testing with Spock
junit5: Testing with JUnit 5
kotest5: Testing with Kotest 5
restAssured: REST Assured
sql: Loading SQL before tests
#spek: Testing with Spek
repository: Repository
3 changes: 3 additions & 0 deletions test-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
83 changes: 83 additions & 0 deletions test-core/src/main/java/io/micronaut/test/annotation/Sql.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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";

/**
* @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();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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;
Expand Down Expand Up @@ -192,6 +193,9 @@ 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);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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.io.ResourceLoader;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.test.annotation.Sql;
import io.micronaut.test.support.sql.processor.SqlScriptProcessor;
import io.micronaut.test.support.sql.resolver.DataSourceResolver;
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;

/**
* Static helper class to handle {@link Sql} annotations.
*
* @since 4.1.0
* @author Tim Yates
*/
@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
* @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) throws SQLException, IOException {
ResourceLoader resourceLoader = applicationContext.getBean(ResourceLoader.class);

var compositeDataSourceHandler = applicationContext.getBean(DataSourceResolver.class);

Optional<List<AnnotationValue<Sql>>> sqlAnnotations = specDefinition
.findAnnotation(Sql.Sqls.class)
.map(s -> s.getAnnotations("value", Sql.class));

if (sqlAnnotations.isPresent()) {
for (var sql : sqlAnnotations.get()) {
String datasourceName = sql.getRequiredValue("datasourceName", String.class);
Class<?> dataSourceType = sql.getRequiredValue("resourceType", Class.class);

Object bean = applicationContext.getBean(dataSourceType, Qualifiers.byName(datasourceName));

if (LOG.isTraceEnabled()) {
LOG.trace("Looking for resource of type {} with name {}", dataSourceType, datasourceName);
}

Optional<? extends SqlScriptProcessor> resolve = compositeDataSourceHandler.resolve(bean);

if (resolve.isPresent()) {
handleScript(
resourceLoader,
Arrays.asList(sql.stringValues("value")),
resolve.get()
);
} else {
LOG.warn("Could not resolve data source: {}", datasourceName);
}
}
}
}

private static void handleScript(
ResourceLoader loader,
List<String> scripts,
SqlScriptProcessor processor
) throws IOException, SQLException {
for (String script : scripts) {
Optional<URL> resource = loader.getResource(script);
if (resource.isPresent()) {
try (InputStream in = resource.get().openStream()) {
String scriptBody = new String(in.readAllBytes(), StandardCharsets.UTF_8);
processor.process(scriptBody);
}
} else {
LOG.warn("Could not find SQL script: {}", script);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.processor;

import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.NonNull;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import java.util.List;

/**
* Processes SQL scripts against an R2DBC {@link ConnectionFactory}.
*
* @since 4.1.0
* @author Tim Yates
*/
@Experimental
public class R2DBCConnectionFactoryProcessor implements SqlScriptProcessor {
timyates marked this conversation as resolved.
Show resolved Hide resolved

private static final Logger LOG = LoggerFactory.getLogger(R2DBCConnectionFactoryProcessor.class);
private final ConnectionFactory connectionFactory;

public R2DBCConnectionFactoryProcessor(ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}

@Override
public void process(@NonNull String sql) {
List<Long> 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);
}
}
}
Loading