diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java index ae11d7d5be4..c2fdc3e7a00 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java @@ -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. * *

Method-level declarations override class-level declarations. * @@ -77,14 +78,14 @@ public @interface Sql { static enum ExecutionPhase { /** - * The configured SQL scripts will be executed before the - * corresponding test method. + * The configured SQL scripts and statements will be executed + * before the corresponding test method. */ BEFORE_TEST_METHOD, /** - * The configured SQL scripts will be executed after the - * corresponding test method. + * The configured SQL scripts and statements will be executed + * after the corresponding test method. */ AFTER_TEST_METHOD } @@ -94,6 +95,8 @@ public @interface Sql { * Alias for {@link #scripts}. *

This attribute may not 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. *

This attribute may not 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}. + * *

Path Resource Semantics

*

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. + * *

Default Script Detection

- *

If no SQL scripts are specified, an attempt will be made to detect a - * default script depending on where this annotation is declared. - * If a default cannot be detected, an {@link IllegalStateException} will be - * thrown. + *

If no SQL scripts or {@link #statements} are specified, an attempt will + * be made to detect a default script depending on where this + * annotation is declared. If a default cannot be detected, an + * {@link IllegalStateException} will be thrown. *

+ * + * @see #value + * @see #statements */ @AliasFor(attribute = "value") String[] scripts() default {}; /** - * When the SQL scripts should be executed. + * Inlined SQL statements to execute. + *

This attribute may be used in conjunction with or instead of + * {@link #scripts}. + * + *

Ordering

+ *

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

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

See the class-level javadocs for {@link SqlConfig} for explanations of * local vs. global configuration, inheritance, overrides, etc. *

Defaults to an empty {@link SqlConfig @SqlConfig} instance. diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index a7de6071939..71e0e1f6390 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -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. * - *

Scripts will be executed {@linkplain #beforeTestMethod(TestContext) before} + *

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

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 + *

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

Script Resources

- *

For details on default script detection and how explicit script locations + *

For details on default script detection and how script resource locations * are interpreted, see {@link Sql#scripts}. * *

Required Spring Beans

@@ -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 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); diff --git a/spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java index 1f2db0eafd7..dd3e74d4e44 100644 --- a/spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java @@ -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 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 convertToResourceList(ResourceLoader resourceLoader, String... paths) { List list = new ArrayList(); for (String path : paths) { list.add(resourceLoader.getResource(path)); } - return list.toArray(new Resource[list.size()]); + return list; } } diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java index ed91023988b..7267b1acf10 100644 --- a/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java @@ -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.> 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.> 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() { diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/TransactionalInlinedStatementsSqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/TransactionalInlinedStatementsSqlScriptsTests.java new file mode 100644 index 00000000000..6a97aea5414 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/TransactionalInlinedStatementsSqlScriptsTests.java @@ -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")); + } + +} diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc index bab1e9c2f31..c807036d099 100644 --- a/src/asciidoc/whats-new.adoc +++ b/src/asciidoc/whats-new.adoc @@ -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 ``, allowing developers to set unique names for embedded databases –- for example, via a SpEL expression or a