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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ public class SqlScriptsTestExecutionListenerTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void missingValueAndScriptsAtClassLevel() throws Exception {
|
||||
Class<?> clazz = MissingValueAndScriptsAtClassLevel.class;
|
||||
public void missingValueAndScriptsAndStatementsAtClassLevel() throws Exception {
|
||||
Class<?> clazz = MissingValueAndScriptsAndStatementsAtClassLevel.class;
|
||||
BDDMockito.<Class<?>> given(testContext.getTestClass()).willReturn(clazz);
|
||||
given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("foo"));
|
||||
|
||||
@@ -59,8 +59,8 @@ public class SqlScriptsTestExecutionListenerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingValueAndScriptsAtMethodLevel() throws Exception {
|
||||
Class<?> clazz = MissingValueAndScriptsAtMethodLevel.class;
|
||||
public void missingValueAndScriptsAndStatementsAtMethodLevel() throws Exception {
|
||||
Class<?> clazz = MissingValueAndScriptsAndStatementsAtMethodLevel.class;
|
||||
BDDMockito.<Class<?>> given(testContext.getTestClass()).willReturn(clazz);
|
||||
given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("foo"));
|
||||
|
||||
@@ -126,13 +126,13 @@ public class SqlScriptsTestExecutionListenerTests {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Sql
|
||||
static class MissingValueAndScriptsAtClassLevel {
|
||||
static class MissingValueAndScriptsAndStatementsAtClassLevel {
|
||||
|
||||
public void foo() {
|
||||
}
|
||||
}
|
||||
|
||||
static class MissingValueAndScriptsAtMethodLevel {
|
||||
static class MissingValueAndScriptsAndStatementsAtMethodLevel {
|
||||
|
||||
@Sql
|
||||
public void foo() {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.jdbc;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.MethodSorters;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.jdbc.JdbcTestUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Transactional integration tests for {@link Sql @Sql} support with
|
||||
* inlined SQL {@link Sql#statements statements}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 4.2
|
||||
* @see TransactionalSqlScriptsTests
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@ContextConfiguration(classes = EmptyDatabaseConfig.class)
|
||||
@Transactional
|
||||
@Sql(
|
||||
scripts = "schema.sql",
|
||||
statements = "INSERT INTO user VALUES('Dilbert')"
|
||||
)
|
||||
@DirtiesContext
|
||||
public class TransactionalInlinedStatementsSqlScriptsTests {
|
||||
|
||||
protected JdbcTemplate jdbcTemplate;
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setDataSource(DataSource dataSource) {
|
||||
this.jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
// test##_ prefix is required for @FixMethodOrder.
|
||||
public void test01_classLevelScripts() {
|
||||
assertNumUsers(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Sql(statements = "DROP TABLE user IF EXISTS")
|
||||
@Sql("schema.sql")
|
||||
@Sql(statements = "INSERT INTO user VALUES ('Dilbert'), ('Dogbert'), ('Catbert')")
|
||||
// test##_ prefix is required for @FixMethodOrder.
|
||||
public void test02_methodLevelScripts() {
|
||||
assertNumUsers(3);
|
||||
}
|
||||
|
||||
protected int countRowsInTable(String tableName) {
|
||||
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
|
||||
}
|
||||
|
||||
protected void assertNumUsers(int expected) {
|
||||
assertEquals("Number of rows in the 'user' table.", expected, countRowsInTable("user"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -573,6 +573,8 @@ public @interface MyTestConfig {
|
||||
_before_ a test -- for example, if some rogue (i.e., yet to be
|
||||
determined) test within a large test suite has corrupted the original
|
||||
configuration for the `ApplicationContext`.
|
||||
* `@Sql` now supports execution of _inlined SQL statements_ via a new
|
||||
`statements` attribute.
|
||||
* The JDBC XML namespace supports a new `database-name` attribute in
|
||||
`<jdbc:embedded-database>`, allowing developers to set unique names
|
||||
for embedded databases –- for example, via a SpEL expression or a
|
||||
|
||||
Reference in New Issue
Block a user