Browse Source

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
pull/826/head
Sam Brannen 11 years ago
parent
commit
10a691bd51
  1. 54
      spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java
  2. 36
      spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java
  3. 25
      spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java
  4. 12
      spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java
  5. 86
      spring-test/src/test/java/org/springframework/test/context/jdbc/TransactionalInlinedStatementsSqlScriptsTests.java
  6. 2
      src/asciidoc/whats-new.adoc

54
spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java

@ -28,8 +28,9 @@ import static java.lang.annotation.ElementType.*; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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.

36
spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java

@ -17,7 +17,9 @@ @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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);

25
spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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;
}
}

12
spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java

@ -50,8 +50,8 @@ public class SqlScriptsTestExecutionListenerTests { @@ -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 { @@ -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 { @@ -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() {

86
spring-test/src/test/java/org/springframework/test/context/jdbc/TransactionalInlinedStatementsSqlScriptsTests.java

@ -0,0 +1,86 @@ @@ -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"));
}
}

2
src/asciidoc/whats-new.adoc

@ -573,6 +573,8 @@ public @interface MyTestConfig { @@ -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

Loading…
Cancel
Save