From 2e75adb04c02b8904c59bd5820336104594ce3fb Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 18 Jul 2014 02:59:03 +0200 Subject: [PATCH] Improve transaction management for @Sql scripts Prior to this commit, the support for SQL script execution via @Sql provided an algorithm for looking up a required PlatformTransactionManager to use to drive transactions. However, a transaction manager is not actually required for all testing scenarios. This commit improves the transaction management support for @Sql so that SQL scripts can be executed without a transaction if a transaction manger is not present in the ApplicationContext. The updated algorithm now supports the following use cases. - If a transaction manager and data source are both present (i.e., explicitly specified via the transactionManager and dataSource attributes of @SqlConfig or implicitly discovered in the ApplicationContext based on conventions), both will be used. - If a transaction manager is not explicitly specified and not implicitly discovered based on conventions, SQL scripts will be executed without a transaction but requiring the presence of a data source. If a data source is not present, an exception will be thrown. - If a data source is not explicitly specified and not implicitly discovered based on conventions, an attempt will be made to retrieve it by using reflection to invoke a public method named getDataSource() on the transaction manager. If this attempt fails, an exception will be thrown. - If a data source can be retrieved from the resolved transaction manager using reflection, an exception will be thrown if the resolved data source is not the data source associated with the resolved transaction manager. This helps to avoid possibly unintended configuration errors. - If @SqlConfig.transactionMode is set to ISOLATED, an exception will be thrown if a transaction manager is not present. Issue: SPR-11911 --- .../test/context/jdbc/Sql.java | 13 +- .../test/context/jdbc/SqlConfig.java | 84 ++++++++---- .../jdbc/SqlScriptsTestExecutionListener.java | 80 ++++++++++-- .../TestContextTransactionUtils.java | 61 ++++++--- .../TransactionalTestExecutionListener.java | 9 +- .../jdbc/DataSourceOnlySqlScriptsTests.java | 92 ++++++++++++++ .../InferredDataSourceSqlScriptsTests.java | 116 +++++++++++++++++ ...ataSourceTransactionalSqlScriptsTests.java | 120 ++++++++++++++++++ .../SqlScriptsTestExecutionListenerTests.java | 52 +++++++- 9 files changed, 557 insertions(+), 70 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/jdbc/DataSourceOnlySqlScriptsTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceSqlScriptsTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceTransactionalSqlScriptsTests.java 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 bc9af8a977..3777aef4e9 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 @@ -22,9 +22,6 @@ import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import org.springframework.jdbc.datasource.init.ScriptUtils; -import org.springframework.util.ResourceUtils; - import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; @@ -39,8 +36,8 @@ import static java.lang.annotation.RetentionPolicy.*; * *

The configuration options provided by this annotation and * {@link SqlConfig @SqlConfig} are equivalent to those supported by - * {@link ScriptUtils} and - * {@link org.springframework.jdbc.datasource.init.ResourceDatabasePopulator} + * {@link org.springframework.jdbc.datasource.init.ScriptUtils ScriptUtils} and + * {@link org.springframework.jdbc.datasource.init.ResourceDatabasePopulator ResourceDatabasePopulator} * but are a superset of those provided by the {@code } * XML namespace element. Consult the javadocs of individual attributes in this * annotation and {@link SqlConfig @SqlConfig} for details. @@ -110,9 +107,9 @@ public @interface Sql { * absolute classpath resource, for example: * {@code "/org/example/schema.sql"}. A path which references a * URL (e.g., a path prefixed with - * {@link ResourceUtils#CLASSPATH_URL_PREFIX classpath:}, - * {@link ResourceUtils#FILE_URL_PREFIX file:}, {@code http:}, etc.) will be - * loaded using the specified resource protocol. + * {@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. diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java index e9ea581310..86f1531a66 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java @@ -21,8 +21,6 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import org.springframework.jdbc.datasource.init.ScriptUtils; - import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; @@ -46,7 +44,7 @@ import static java.lang.annotation.RetentionPolicy.*; * attribute. Thus, in order to support overrides of inherited global * configuration, {@code @SqlConfig} attributes have an explicit * {@code default} value of either {@code ""} for Strings or {@code DEFAULT} for - * Enums. This approach allows local declarations {@code @SqlConfig} to + * Enums. This approach allows local declarations of {@code @SqlConfig} to * selectively override individual attributes from global declarations of * {@code @SqlConfig} by providing a value other than {@code ""} or {@code DEFAULT}. * @@ -91,22 +89,50 @@ public @interface SqlConfig { DEFAULT, /** - * Indicates that the transaction mode to use when executing SQL scripts - * should be inferred based on whether or not a Spring-managed - * transaction is currently present. - *

SQL scripts will be executed within the current transaction if present; - * otherwise, scripts will be executed in a new transaction that will be - * immediately committed. - *

The current transaction will typically be managed by the - * {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener - * TransactionalTestExecutionListener}. + * Indicates that the transaction mode to use when executing SQL + * scripts should be inferred using the rules listed below. + * In the context of these rules, the term "available" + * means that the bean for the data source or transaction manager + * is either explicitly specified via a corresponding annotation + * attribute in {@code @SqlConfig} or discoverable via conventions. See + * {@link org.springframework.test.context.transaction.TestContextTransactionUtils TestContextTransactionUtils} + * for details on the conventions used to discover such beans in + * the {@code ApplicationContext}. + * + *

Inference Rules

+ *
    + *
  1. If neither a transaction manager nor a data source is + * available, an exception will be thrown. + *
  2. If a transaction manager is not available but a data source + * is available, SQL scripts will be executed directly against the + * data source without a transaction. + *
  3. If a transaction manager is available: + *
      + *
    • If a data source is not available, an attempt will be made + * to retrieve it from the transaction manager by using reflection + * to invoke a public method named {@code getDataSource()} on the + * transaction manager. If the attempt fails, an exception will be + * thrown. + *
    • Using the resolved transaction manager and data source, SQL + * scripts will be executed within an existing transaction if + * present; otherwise, scripts will be executed in a new transaction + * that will be immediately committed. An existing + * transaction will typically be managed by the + * {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener TransactionalTestExecutionListener}. + *
    + *
* @see #ISOLATED + * @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveDataSource + * @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveTransactionManager */ INFERRED, /** * Indicates that SQL scripts should always be executed in a new, * isolated transaction that will be immediately committed. + *

In contrast to {@link #INFERRED}, this mode requires the + * presence of a transaction manager and a data + * source. */ ISOLATED } @@ -164,18 +190,24 @@ public @interface SqlConfig { /** - * The bean name of the {@link javax.sql.DataSource} against which the scripts - * should be executed. - *

The name is only used if there is more than one bean of type - * {@code DataSource} in the test's {@code ApplicationContext}. If there is - * only one such bean, it is not necessary to specify a bean name. + * The bean name of the {@link javax.sql.DataSource} against which the + * scripts should be executed. + *

The name is only required if there is more than one bean of type + * {@code DataSource} in the test's {@code ApplicationContext}. If there + * is only one such bean, it is not necessary to specify a bean name. *

Defaults to an empty string, requiring that one of the following is * true: *

    + *
  1. An explicit bean name is defined in a global declaration of + * {@code @SqlConfig}. + *
  2. The data source can be retrieved from the transaction manager + * by using reflection to invoke a public method named + * {@code getDataSource()} on the transaction manager. *
  3. There is only one bean of type {@code DataSource} in the test's * {@code ApplicationContext}.
  4. *
  5. The {@code DataSource} to use is named {@code "dataSource"}.
  6. *
+ * @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveDataSource */ String dataSource() default ""; @@ -188,6 +220,8 @@ public @interface SqlConfig { *

Defaults to an empty string, requiring that one of the following is * true: *

    + *
  1. An explicit bean name is defined in a global declaration of + * {@code @SqlConfig}. *
  2. There is only one bean of type {@code PlatformTransactionManager} in * the test's {@code ApplicationContext}.
  3. *
  4. {@link org.springframework.transaction.annotation.TransactionManagementConfigurer @@ -197,6 +231,7 @@ public @interface SqlConfig { *
  5. The {@code PlatformTransactionManager} to use is named * {@code "transactionManager"}.
  6. *
+ * @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveTransactionManager */ String transactionManager() default ""; @@ -223,16 +258,19 @@ public @interface SqlConfig { * SQL scripts. *

Implicitly defaults to {@code ";"} if not specified and falls back to * {@code "\n"} as a last resort. - *

May be set to {@link ScriptUtils#EOF_STATEMENT_SEPARATOR} to signal - * that each script contains a single statement without a separator. - * @see ScriptUtils#DEFAULT_STATEMENT_SEPARATOR + *

May be set to + * {@link org.springframework.jdbc.datasource.init.ScriptUtils#EOF_STATEMENT_SEPARATOR} + * to signal that each script contains a single statement without a + * separator. + * @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_STATEMENT_SEPARATOR + * @see org.springframework.jdbc.datasource.init.ScriptUtils#EOF_STATEMENT_SEPARATOR */ String separator() default ""; /** * The prefix that identifies single-line comments within the SQL scripts. *

Implicitly defaults to {@code "--"}. - * @see ScriptUtils#DEFAULT_COMMENT_PREFIX + * @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_COMMENT_PREFIX */ String commentPrefix() default ""; @@ -240,7 +278,7 @@ public @interface SqlConfig { * The start delimiter that identifies block comments within the SQL scripts. *

Implicitly defaults to {@code "/*"}. * @see #blockCommentEndDelimiter - * @see ScriptUtils#DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_BLOCK_COMMENT_START_DELIMITER */ String blockCommentStartDelimiter() default ""; @@ -248,7 +286,7 @@ public @interface SqlConfig { * The end delimiter that identifies block comments within the SQL scripts. *

Implicitly defaults to "*/". * @see #blockCommentStartDelimiter - * @see ScriptUtils#DEFAULT_BLOCK_COMMENT_END_DELIMITER + * @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_BLOCK_COMMENT_END_DELIMITER */ String blockCommentEndDelimiter() default ""; 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 8bab99cead..6f75cc068e 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 @@ -43,6 +43,7 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.ResourceUtils; /** @@ -165,24 +166,77 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scripts)); } - final DataSource dataSource = TestContextTransactionUtils.retrieveDataSource(testContext, - mergedSqlConfig.getDataSource()); + String dsName = mergedSqlConfig.getDataSource(); + String tmName = mergedSqlConfig.getTransactionManager(); + DataSource dataSource = TestContextTransactionUtils.retrieveDataSource(testContext, dsName); final PlatformTransactionManager transactionManager = TestContextTransactionUtils.retrieveTransactionManager( - testContext, mergedSqlConfig.getTransactionManager()); + testContext, tmName); + final boolean newTxRequired = mergedSqlConfig.getTransactionMode() == TransactionMode.ISOLATED; - int propagation = (mergedSqlConfig.getTransactionMode() == TransactionMode.ISOLATED) ? TransactionDefinition.PROPAGATION_REQUIRES_NEW - : TransactionDefinition.PROPAGATION_REQUIRED; + if (transactionManager == null) { + if (newTxRequired) { + throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: " + + "cannot execute SQL scripts using Transaction Mode " + + "[%s] without a PlatformTransactionManager.", testContext, TransactionMode.ISOLATED)); + } - TransactionAttribute transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute( - testContext, new DefaultTransactionAttribute(propagation)); + if (dataSource == null) { + throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: " + + "supply at least a DataSource or PlatformTransactionManager.", testContext)); + } - new TransactionTemplate(transactionManager, transactionAttribute).execute(new TransactionCallbackWithoutResult() { + // Execute scripts directly against the DataSource + populator.execute(dataSource); + } + else { + DataSource dataSourceFromTxMgr = getDataSourceFromTransactionManager(transactionManager); - @Override - public void doInTransactionWithoutResult(TransactionStatus status) { - populator.execute(dataSource); - }; - }); + // Ensure user configured an appropriate DataSource/TransactionManager pair. + if ((dataSource != null) && (dataSourceFromTxMgr != null) && !dataSource.equals(dataSourceFromTxMgr)) { + throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: " + + "the configured DataSource [%s] (named '%s') is not the one associated " + + "with transaction manager [%s] (named '%s').", testContext, dataSource.getClass().getName(), + dsName, transactionManager.getClass().getName(), tmName)); + } + + if (dataSource == null) { + dataSource = dataSourceFromTxMgr; + if (dataSource == null) { + throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: " + + "could not obtain DataSource from transaction manager [%s] (named '%s').", testContext, + transactionManager.getClass().getName(), tmName)); + } + } + + final DataSource finalDataSource = dataSource; + int propagation = newTxRequired ? TransactionDefinition.PROPAGATION_REQUIRES_NEW + : TransactionDefinition.PROPAGATION_REQUIRED; + + TransactionAttribute transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute( + testContext, new DefaultTransactionAttribute(propagation)); + + new TransactionTemplate(transactionManager, transactionAttribute).execute(new TransactionCallbackWithoutResult() { + + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + populator.execute(finalDataSource); + } + }); + } + } + + private DataSource getDataSourceFromTransactionManager(PlatformTransactionManager transactionManager) { + try { + Method getDataSourceMethod = transactionManager.getClass().getMethod("getDataSource"); + Object obj = ReflectionUtils.invokeMethod(getDataSourceMethod, transactionManager); + if (obj instanceof DataSource) { + return (DataSource) obj; + } + } + catch (Exception e) { + /* ignore */ + } + return null; } private String[] getScripts(Sql sql, TestContext testContext, boolean classLevel) { diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java index df8e813c66..1a75a9dee0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java @@ -66,21 +66,23 @@ public abstract class TestContextTransactionUtils { /** * Retrieve the {@link DataSource} to use for the supplied {@linkplain TestContext * test context}. - *

The following algorithm is used to retrieve the {@code DataSource} - * from the {@link org.springframework.context.ApplicationContext ApplicationContext} + *

The following algorithm is used to retrieve the {@code DataSource} from + * the {@link org.springframework.context.ApplicationContext ApplicationContext} * of the supplied test context: *

    *
  1. Look up the {@code DataSource} by type and name, if the supplied - * {@code name} is non-empty. - *
  2. Look up the {@code DataSource} by type. - *
  3. Look up the {@code DataSource} by type and the + * {@code name} is non-empty, throwing a {@link BeansException} if the named + * {@code DataSource} does not exist. + *
  4. Attempt to look up a single {@code DataSource} by type. + *
  5. Attempt to look up the {@code DataSource} by type and the * {@linkplain #DEFAULT_DATA_SOURCE_NAME default data source name}. * @param testContext the test context for which the {@code DataSource} * should be retrieved; never {@code null} * @param name the name of the {@code DataSource} to retrieve; may be {@code null} * or empty - * @return the {@code DataSource} to use - * @throws BeansException if an error occurs while retrieving the {@code DataSource} + * @return the {@code DataSource} to use, or {@code null} if not found + * @throws BeansException if an error occurs while retrieving an explicitly + * named {@code DataSource} */ public static DataSource retrieveDataSource(TestContext testContext, String name) { Assert.notNull(testContext, "TestContext must not be null"); @@ -91,7 +93,14 @@ public abstract class TestContextTransactionUtils { if (StringUtils.hasText(name)) { return bf.getBean(name, DataSource.class); } + } + catch (BeansException ex) { + logger.error( + String.format("Failed to retrieve DataSource named '%s' for test context %s", name, testContext), ex); + throw ex; + } + try { if (bf instanceof ListableBeanFactory) { ListableBeanFactory lbf = (ListableBeanFactory) bf; @@ -107,10 +116,10 @@ public abstract class TestContextTransactionUtils { return bf.getBean(DEFAULT_DATA_SOURCE_NAME, DataSource.class); } catch (BeansException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Caught exception while retrieving DataSource for test context " + testContext, ex); + if (logger.isDebugEnabled()) { + logger.debug("Caught exception while retrieving DataSource for test context " + testContext, ex); } - throw ex; + return null; } } @@ -121,20 +130,22 @@ public abstract class TestContextTransactionUtils { * from the {@link org.springframework.context.ApplicationContext ApplicationContext} * of the supplied test context: *
      - *
    1. Look up the transaction manager by type and name, if the supplied - * {@code name} is non-empty. - *
    2. Look up the transaction manager by type. - *
    3. Look up the transaction manager via a {@link TransactionManagementConfigurer}, - * if present. - *
    4. Look up the transaction manager by type and the + *
    5. Look up the transaction manager by type and explicit name, if the supplied + * {@code name} is non-empty, throwing a {@link BeansException} if the named + * transaction manager does not exist. + *
    6. Attempt to look up the transaction manager by type. + *
    7. Attempt to look up the transaction manager via a + * {@link TransactionManagementConfigurer}, if present. + *
    8. Attempt to look up the transaction manager by type and the * {@linkplain #DEFAULT_TRANSACTION_MANAGER_NAME default transaction manager * name}. * @param testContext the test context for which the transaction manager * should be retrieved; never {@code null} * @param name the name of the transaction manager to retrieve; may be * {@code null} or empty - * @return the transaction manager to use - * @throws BeansException if an error occurs while retrieving the transaction manager + * @return the transaction manager to use, or {@code null} if not found + * @throws BeansException if an error occurs while retrieving an explicitly + * named transaction manager */ public static PlatformTransactionManager retrieveTransactionManager(TestContext testContext, String name) { Assert.notNull(testContext, "TestContext must not be null"); @@ -145,7 +156,14 @@ public abstract class TestContextTransactionUtils { if (StringUtils.hasText(name)) { return bf.getBean(name, PlatformTransactionManager.class); } + } + catch (BeansException ex) { + logger.error(String.format("Failed to retrieve transaction manager named '%s' for test context %s", name, + testContext), ex); + throw ex; + } + try { if (bf instanceof ListableBeanFactory) { ListableBeanFactory lbf = (ListableBeanFactory) bf; @@ -172,10 +190,11 @@ public abstract class TestContextTransactionUtils { return bf.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class); } catch (BeansException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Caught exception while retrieving transaction manager for test context " + testContext, ex); + if (logger.isDebugEnabled()) { + logger.debug("Caught exception while retrieving transaction manager for test context " + testContext, + ex); } - throw ex; + return null; } } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 8e7bf5e37f..0cf9cab385 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -275,8 +275,10 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis } catch (RuntimeException ex) { if (logger.isWarnEnabled()) { - logger.warn("Caught exception while retrieving transaction manager for test context " + testContext - + " and qualifier [" + qualifier + "]", ex); + logger.warn( + String.format( + "Caught exception while retrieving transaction manager with qualifier '%s' for test context %s", + qualifier, testContext), ex); } throw ex; } @@ -294,7 +296,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis * @param testContext the test context for which the transaction manager * should be retrieved * @return the transaction manager to use, or {@code null} if not found - * @throws BeansException if an error occurs while retrieving the transaction manager + * @throws BeansException if an error occurs while retrieving an explicitly + * named transaction manager * @see #getTransactionManager(TestContext, String) */ protected PlatformTransactionManager getTransactionManager(TestContext testContext) { diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/DataSourceOnlySqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/DataSourceOnlySqlScriptsTests.java new file mode 100644 index 0000000000..29a27b5370 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/DataSourceOnlySqlScriptsTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2014 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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +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 static org.junit.Assert.*; +import static org.springframework.test.transaction.TransactionTestUtils.*; + +/** + * Integration tests for {@link Sql @Sql} support with only a {@link DataSource} + * present in the context (i.e., no transaction manager). + * + * @author Sam Brannen + * @since 4.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@ContextConfiguration +@Sql({ "schema.sql", "data.sql" }) +@DirtiesContext +public class DataSourceOnlySqlScriptsTests { + + private JdbcTemplate jdbcTemplate; + + + @Autowired + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Test + // test##_ prefix is required for @FixMethodOrder. + public void test01_classLevelScripts() { + assertInTransaction(false); + assertNumUsers(1); + } + + @Test + @Sql({ "drop-schema.sql", "schema.sql", "data.sql", "data-add-dogbert.sql" }) + // test##_ prefix is required for @FixMethodOrder. + public void test02_methodLevelScripts() { + assertInTransaction(false); + assertNumUsers(2); + } + + protected void assertNumUsers(int expected) { + assertEquals("Number of rows in the 'user' table.", expected, + JdbcTestUtils.countRowsInTable(jdbcTemplate, "user")); + } + + + @Configuration + static class Config { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder()// + .setName("empty-sql-scripts-without-tx-mgr-test-db")// + .build(); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceSqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceSqlScriptsTests.java new file mode 100644 index 0000000000..649304b587 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceSqlScriptsTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2014 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 java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.junit.Assert.*; +import static org.springframework.test.transaction.TransactionTestUtils.*; + +/** + * Integration tests for {@link Sql @Sql} that verify support for inferring + * {@link DataSource}s from {@link PlatformTransactionManager}s. + * + * @author Sam Brannen + * @since 4.1 + * @see InferredDataSourceTransactionalSqlScriptsTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@DirtiesContext +public class InferredDataSourceSqlScriptsTests { + + @Autowired + private DataSource dataSource1; + + @Autowired + private DataSource dataSource2; + + + @Test + @Sql(scripts = "data-add-dogbert.sql", config = @SqlConfig(transactionManager = "txMgr1")) + public void database1() { + assertInTransaction(false); + assertUsers(new JdbcTemplate(dataSource1), "Dilbert", "Dogbert"); + } + + @Test + @Sql(scripts = "data-add-catbert.sql", config = @SqlConfig(transactionManager = "txMgr2")) + public void database2() { + assertInTransaction(false); + assertUsers(new JdbcTemplate(dataSource2), "Dilbert", "Catbert"); + } + + private void assertUsers(JdbcTemplate jdbcTemplate, String... users) { + List expected = Arrays.asList(users); + Collections.sort(expected); + List actual = jdbcTemplate.queryForList("select name from user", String.class); + Collections.sort(actual); + assertEquals("Users in database;", expected, actual); + } + + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager txMgr1() { + return new DataSourceTransactionManager(dataSource1()); + } + + @Bean + public PlatformTransactionManager txMgr2() { + return new DataSourceTransactionManager(dataSource2()); + } + + @Bean + public DataSource dataSource1() { + return new EmbeddedDatabaseBuilder()// + .setName("database1")// + .addScript("classpath:/org/springframework/test/context/jdbc/schema.sql")// + .addScript("classpath:/org/springframework/test/context/jdbc/data.sql")// + .build(); + } + + @Bean + public DataSource dataSource2() { + return new EmbeddedDatabaseBuilder()// + .setName("database2")// + .addScript("classpath:/org/springframework/test/context/jdbc/schema.sql")// + .addScript("classpath:/org/springframework/test/context/jdbc/data.sql")// + .build(); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceTransactionalSqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceTransactionalSqlScriptsTests.java new file mode 100644 index 0000000000..ef3c306cbd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/InferredDataSourceTransactionalSqlScriptsTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2014 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 java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.transaction.TransactionTestUtils.*; + +import static org.junit.Assert.*; + +/** + * Exact copy of {@link InferredDataSourceSqlScriptsTests}, except that test + * methods are transactional. + * + * @author Sam Brannen + * @since 4.1 + * @see InferredDataSourceSqlScriptsTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@DirtiesContext +public class InferredDataSourceTransactionalSqlScriptsTests { + + @Autowired + private DataSource dataSource1; + + @Autowired + private DataSource dataSource2; + + + @Test + @Transactional("txMgr1") + @Sql(scripts = "data-add-dogbert.sql", config = @SqlConfig(transactionManager = "txMgr1")) + public void database1() { + assertInTransaction(true); + assertUsers(new JdbcTemplate(dataSource1), "Dilbert", "Dogbert"); + } + + @Test + @Transactional("txMgr2") + @Sql(scripts = "data-add-catbert.sql", config = @SqlConfig(transactionManager = "txMgr2")) + public void database2() { + assertInTransaction(true); + assertUsers(new JdbcTemplate(dataSource2), "Dilbert", "Catbert"); + } + + private void assertUsers(JdbcTemplate jdbcTemplate, String... users) { + List expected = Arrays.asList(users); + Collections.sort(expected); + List actual = jdbcTemplate.queryForList("select name from user", String.class); + Collections.sort(actual); + assertEquals("Users in database;", expected, actual); + } + + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager txMgr1() { + return new DataSourceTransactionManager(dataSource1()); + } + + @Bean + public PlatformTransactionManager txMgr2() { + return new DataSourceTransactionManager(dataSource2()); + } + + @Bean + public DataSource dataSource1() { + return new EmbeddedDatabaseBuilder()// + .setName("database1")// + .addScript("classpath:/org/springframework/test/context/jdbc/schema.sql")// + .addScript("classpath:/org/springframework/test/context/jdbc/data.sql")// + .build(); + } + + @Bean + public DataSource dataSource2() { + return new EmbeddedDatabaseBuilder()// + .setName("database2")// + .addScript("classpath:/org/springframework/test/context/jdbc/schema.sql")// + .addScript("classpath:/org/springframework/test/context/jdbc/data.sql")// + .build(); + } + } + +} 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 58d2a6d60d..c90d5f1572 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 @@ -18,9 +18,14 @@ package org.springframework.test.context.jdbc; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; import org.springframework.test.context.TestContext; +import org.springframework.test.context.jdbc.SqlConfig.TransactionMode; import static org.junit.Assert.*; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; /** @@ -58,17 +63,46 @@ public class SqlScriptsTestExecutionListenerTests { public void valueAndScriptsDeclared() throws Exception { Class clazz = ValueAndScriptsDeclared.class; Mockito.> when(testContext.getTestClass()).thenReturn(clazz); - when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("valueAndScriptsDeclared")); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("foo")); assertExceptionContains("Only one declaration of SQL script paths is permitted"); } + @Test + public void isolatedTxModeDeclaredWithoutTxMgr() throws Exception { + ApplicationContext ctx = mock(ApplicationContext.class); + when(ctx.getResource(anyString())).thenReturn(mock(Resource.class)); + when(ctx.getAutowireCapableBeanFactory()).thenReturn(mock(AutowireCapableBeanFactory.class)); + + Class clazz = IsolatedWithoutTxMgr.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("foo")); + when(testContext.getApplicationContext()).thenReturn(ctx); + + assertExceptionContains("cannot execute SQL scripts using Transaction Mode [ISOLATED] without a PlatformTransactionManager"); + } + + @Test + public void missingDataSourceAndTxMgr() throws Exception { + ApplicationContext ctx = mock(ApplicationContext.class); + when(ctx.getResource(anyString())).thenReturn(mock(Resource.class)); + when(ctx.getAutowireCapableBeanFactory()).thenReturn(mock(AutowireCapableBeanFactory.class)); + + Class clazz = MissingDataSourceAndTxMgr.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("foo")); + when(testContext.getApplicationContext()).thenReturn(ctx); + + assertExceptionContains("supply at least a DataSource or PlatformTransactionManager"); + } + private void assertExceptionContains(String msg) throws Exception { try { listener.beforeTestMethod(testContext); fail("Should have thrown an IllegalStateException."); } catch (IllegalStateException e) { + // System.err.println(e.getMessage()); assertTrue("Exception message should contain: " + msg, e.getMessage().contains(msg)); } } @@ -93,7 +127,21 @@ public class SqlScriptsTestExecutionListenerTests { static class ValueAndScriptsDeclared { @Sql(value = "foo", scripts = "bar") - public void valueAndScriptsDeclared() { + public void foo() { + } + } + + static class IsolatedWithoutTxMgr { + + @Sql(scripts = "foo.sql", config = @SqlConfig(transactionMode = TransactionMode.ISOLATED)) + public void foo() { + } + } + + static class MissingDataSourceAndTxMgr { + + @Sql("foo.sql") + public void foo() { } }