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:
Sam Brannen
2015-06-23 20:36:37 +02:00
parent 3da59178e5
commit 10a691bd51
6 changed files with 181 additions and 34 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -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;
}
}