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: + * + *
* @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() { } }