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
This commit is contained in:
Sam Brannen
2014-07-18 02:59:03 +02:00
parent 44e4569150
commit 2e75adb04c
9 changed files with 557 additions and 70 deletions

View File

@@ -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();
}
}
}

View File

@@ -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<String> expected = Arrays.asList(users);
Collections.sort(expected);
List<String> 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();
}
}
}

View File

@@ -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<String> expected = Arrays.asList(users);
Collections.sort(expected);
List<String> 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();
}
}
}

View File

@@ -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.<Class<?>> 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.<Class<?>> 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.<Class<?>> 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() {
}
}