Support inlined SQL statements in @Sql
Prior to this commit, it was only possible to declare SQL statements via @Sql within external script resources (i.e., classpath or file system resources); however, many developers have inquired about the ability to inline SQL statements with @Sql analogous to the support for inlined properties in @TestPropertySource. This commit introduces support for declaring _inlined SQL statements_ in `@Sql` via a new `statements` attribute. Inlined statements are executed after statements in scripts. Issue: SPR-13159
This commit is contained in:
@@ -28,8 +28,9 @@ import static java.lang.annotation.ElementType.*;
|
||||
import static java.lang.annotation.RetentionPolicy.*;
|
||||
|
||||
/**
|
||||
* {@code @Sql} is used to annotate a test class or test method to configure SQL
|
||||
* scripts to be executed against a given database during integration tests.
|
||||
* {@code @Sql} is used to annotate a test class or test method to configure
|
||||
* SQL {@link #scripts} and {@link #statements} to be executed against a given
|
||||
* database during integration tests.
|
||||
*
|
||||
* <p>Method-level declarations override class-level declarations.
|
||||
*
|
||||
@@ -77,14 +78,14 @@ public @interface Sql {
|
||||
static enum ExecutionPhase {
|
||||
|
||||
/**
|
||||
* The configured SQL scripts will be executed <em>before</em> the
|
||||
* corresponding test method.
|
||||
* The configured SQL scripts and statements will be executed
|
||||
* <em>before</em> the corresponding test method.
|
||||
*/
|
||||
BEFORE_TEST_METHOD,
|
||||
|
||||
/**
|
||||
* The configured SQL scripts will be executed <em>after</em> the
|
||||
* corresponding test method.
|
||||
* The configured SQL scripts and statements will be executed
|
||||
* <em>after</em> the corresponding test method.
|
||||
*/
|
||||
AFTER_TEST_METHOD
|
||||
}
|
||||
@@ -94,6 +95,8 @@ public @interface Sql {
|
||||
* Alias for {@link #scripts}.
|
||||
* <p>This attribute may <strong>not</strong> be used in conjunction with
|
||||
* {@link #scripts}, but it may be used instead of {@link #scripts}.
|
||||
* @see #scripts
|
||||
* @see #statements
|
||||
*/
|
||||
@AliasFor(attribute = "scripts")
|
||||
String[] value() default {};
|
||||
@@ -101,7 +104,10 @@ public @interface Sql {
|
||||
/**
|
||||
* The paths to the SQL scripts to execute.
|
||||
* <p>This attribute may <strong>not</strong> be used in conjunction with
|
||||
* {@link #value}, but it may be used instead of {@link #value}.
|
||||
* {@link #value}, but it may be used instead of {@link #value}. Similarly,
|
||||
* this attribute may be used in conjunction with or instead of
|
||||
* {@link #statements}.
|
||||
*
|
||||
* <h3>Path Resource Semantics</h3>
|
||||
* <p>Each path will be interpreted as a Spring
|
||||
* {@link org.springframework.core.io.Resource Resource}. A plain path
|
||||
@@ -114,11 +120,12 @@ public @interface Sql {
|
||||
* {@link org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX classpath:},
|
||||
* {@link org.springframework.util.ResourceUtils#FILE_URL_PREFIX file:},
|
||||
* {@code http:}, etc.) will be loaded using the specified resource protocol.
|
||||
*
|
||||
* <h3>Default Script Detection</h3>
|
||||
* <p>If no SQL scripts are specified, an attempt will be made to detect a
|
||||
* <em>default</em> script depending on where this annotation is declared.
|
||||
* If a default cannot be detected, an {@link IllegalStateException} will be
|
||||
* thrown.
|
||||
* <p>If no SQL scripts or {@link #statements} are specified, an attempt will
|
||||
* be made to detect a <em>default</em> script depending on where this
|
||||
* annotation is declared. If a default cannot be detected, an
|
||||
* {@link IllegalStateException} will be thrown.
|
||||
* <ul>
|
||||
* <li><strong>class-level declaration</strong>: if the annotated test class
|
||||
* is {@code com.example.MyTest}, the corresponding default script is
|
||||
@@ -128,19 +135,38 @@ public @interface Sql {
|
||||
* {@code com.example.MyTest}, the corresponding default script is
|
||||
* {@code "classpath:com/example/MyTest.testMethod.sql"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see #value
|
||||
* @see #statements
|
||||
*/
|
||||
@AliasFor(attribute = "value")
|
||||
String[] scripts() default {};
|
||||
|
||||
/**
|
||||
* When the SQL scripts should be executed.
|
||||
* <em>Inlined SQL statements</em> to execute.
|
||||
* <p>This attribute may be used in conjunction with or instead of
|
||||
* {@link #scripts}.
|
||||
*
|
||||
* <h3>Ordering</h3>
|
||||
* <p>Statements declared via this attribute will be executed after
|
||||
* statements loaded from resource {@link #scripts}. If you wish to have
|
||||
* inlined statements executed before scripts, simply declare multiple
|
||||
* instances of {@code @Sql} on the same class or method.
|
||||
*
|
||||
* @since 4.2
|
||||
* @see #scripts
|
||||
*/
|
||||
String[] statements() default {};
|
||||
|
||||
/**
|
||||
* When the SQL scripts and statements should be executed.
|
||||
* <p>Defaults to {@link ExecutionPhase#BEFORE_TEST_METHOD BEFORE_TEST_METHOD}.
|
||||
*/
|
||||
ExecutionPhase executionPhase() default ExecutionPhase.BEFORE_TEST_METHOD;
|
||||
|
||||
/**
|
||||
* Local configuration for the SQL scripts declared within this
|
||||
* {@code @Sql} annotation.
|
||||
* Local configuration for the SQL scripts and statements declared within
|
||||
* this {@code @Sql} annotation.
|
||||
* <p>See the class-level javadocs for {@link SqlConfig} for explanations of
|
||||
* local vs. global configuration, inheritance, overrides, etc.
|
||||
* <p>Defaults to an empty {@link SqlConfig @SqlConfig} instance.
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package org.springframework.test.context.jdbc;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -25,7 +27,9 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.springframework.test.context.TestContext;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
@@ -45,23 +49,25 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@code TestExecutionListener} that provides support for executing SQL scripts
|
||||
* {@code TestExecutionListener} that provides support for executing SQL
|
||||
* {@link Sql#scripts scripts} and inlined {@link Sql#statements statements}
|
||||
* configured via the {@link Sql @Sql} annotation.
|
||||
*
|
||||
* <p>Scripts will be executed {@linkplain #beforeTestMethod(TestContext) before}
|
||||
* <p>Scripts and inlined statements will be executed {@linkplain #beforeTestMethod(TestContext) before}
|
||||
* or {@linkplain #afterTestMethod(TestContext) after} execution of the corresponding
|
||||
* {@linkplain java.lang.reflect.Method test method}, depending on the configured
|
||||
* value of the {@link Sql#executionPhase executionPhase} flag.
|
||||
*
|
||||
* <p>Scripts will be executed without a transaction, within an existing
|
||||
* Spring-managed transaction, or within an isolated transaction, depending
|
||||
* on the configured value of {@link SqlConfig#transactionMode} and the
|
||||
* <p>Scripts and inlined statements will be executed without a transaction,
|
||||
* within an existing Spring-managed transaction, or within an isolated transaction,
|
||||
* depending on the configured value of {@link SqlConfig#transactionMode} and the
|
||||
* presence of a transaction manager.
|
||||
*
|
||||
* <h3>Script Resources</h3>
|
||||
* <p>For details on default script detection and how explicit script locations
|
||||
* <p>For details on default script detection and how script resource locations
|
||||
* are interpreted, see {@link Sql#scripts}.
|
||||
*
|
||||
* <h3>Required Spring Beans</h3>
|
||||
@@ -175,9 +181,19 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
|
||||
|
||||
String[] scripts = getScripts(sql, testContext, classLevel);
|
||||
scripts = TestContextResourceUtils.convertToClasspathResourcePaths(testContext.getTestClass(), scripts);
|
||||
populator.setScripts(TestContextResourceUtils.convertToResources(testContext.getApplicationContext(), scripts));
|
||||
List<Resource> scriptResources = TestContextResourceUtils.convertToResourceList(
|
||||
testContext.getApplicationContext(), scripts);
|
||||
|
||||
for (String statement : sql.statements()) {
|
||||
if (StringUtils.hasText(statement)) {
|
||||
statement = statement.trim();
|
||||
scriptResources.add(new ByteArrayResource(statement.getBytes(), "from inlined SQL statement: " + statement));
|
||||
}
|
||||
}
|
||||
|
||||
populator.setScripts(scriptResources.toArray(new Resource[scriptResources.size()]));
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scripts));
|
||||
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scriptResources));
|
||||
}
|
||||
|
||||
String dsName = mergedSqlConfig.getDataSource();
|
||||
@@ -255,7 +271,7 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
|
||||
|
||||
private String[] getScripts(Sql sql, TestContext testContext, boolean classLevel) {
|
||||
String[] scripts = sql.scripts();
|
||||
if (ObjectUtils.isEmpty(scripts)) {
|
||||
if (ObjectUtils.isEmpty(scripts) && ObjectUtils.isEmpty(sql.statements())) {
|
||||
scripts = new String[] { detectDefaultScript(testContext, classLevel) };
|
||||
}
|
||||
return scripts;
|
||||
@@ -289,7 +305,7 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
|
||||
}
|
||||
else {
|
||||
String msg = String.format("Could not detect default SQL script for test %s [%s]: "
|
||||
+ "%s does not exist. Either declare scripts via @Sql or make the "
|
||||
+ "%s does not exist. Either declare statements or scripts via @Sql or make the "
|
||||
+ "default SQL script available.", elementType, elementName, classPathResource);
|
||||
logger.error(msg);
|
||||
throw new IllegalStateException(msg);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 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.
|
||||
@@ -88,20 +88,37 @@ public abstract class TestContextResourceUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the supplied paths to {@link Resource} handles using the given
|
||||
* {@link ResourceLoader}.
|
||||
* Convert the supplied paths to an array of {@link Resource} handles using
|
||||
* the given {@link ResourceLoader}.
|
||||
*
|
||||
* @param resourceLoader the {@code ResourceLoader} to use to convert the paths
|
||||
* @param paths the paths to be converted
|
||||
* @return a new array of resources
|
||||
* @see #convertToResourceList(ResourceLoader, String...)
|
||||
* @see #convertToClasspathResourcePaths
|
||||
*/
|
||||
public static Resource[] convertToResources(ResourceLoader resourceLoader, String... paths) {
|
||||
List<Resource> list = convertToResourceList(resourceLoader, paths);
|
||||
return list.toArray(new Resource[list.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the supplied paths to a list of {@link Resource} handles using
|
||||
* the given {@link ResourceLoader}.
|
||||
*
|
||||
* @param resourceLoader the {@code ResourceLoader} to use to convert the paths
|
||||
* @param paths the paths to be converted
|
||||
* @return a new list of resources
|
||||
* @since 4.2
|
||||
* @see #convertToResources(ResourceLoader, String...)
|
||||
* @see #convertToClasspathResourcePaths
|
||||
*/
|
||||
public static List<Resource> convertToResourceList(ResourceLoader resourceLoader, String... paths) {
|
||||
List<Resource> list = new ArrayList<Resource>();
|
||||
for (String path : paths) {
|
||||
list.add(resourceLoader.getResource(path));
|
||||
}
|
||||
return list.toArray(new Resource[list.size()]);
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user