-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Investigate claims made in SPR-9051 regarding transactional tests
The claim: given an integration test class that is annotated with @ContextConfiguration and declares a configuration class that is missing an @configuration annotation, if a transactional test method (i.e., one annotated with @transactional) changes the state of the database then the changes will not be rolled back as would be expected with the default rollback semantics of the Spring TestContext Framework (TCF). TransactionalAnnotatedConfigClassWithAtConfigurationTests is a concrete implementation of AbstractTransactionalAnnotatedConfigClassTests that uses a true @configuration class and thereby demonstrates the expected behavior of such transactional tests with automatic rollback. TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests is a concrete implementation of AbstractTransactionalAnnotatedConfigClassTests that does NOT use a true @configuration class but rather a 'lite mode' configuration class (see the Javadoc for @bean for details). Using such a 'lite mode' configuration class results in the following: - Its @bean methods act as factory methods instead of singleton beans. - The dataSource() method is invoked multiple times instead of once. - The test instance and the TCF operate on different data sources. - The transaction managed (and rolled back) by the TCF is not the transaction that the application code or test instance uses. Ultimately, the use of a 'lite mode' configuration class gives the false appearance that there is a bug in the TCF (in that the transaction is not rolled back); however, the transaction managed by the TCF is in fact rolled back. In conclusion, these tests demonstrate both the intended behavior of the TCF and the fact that using 'lite mode' configuration classes can lead to confusing results (both in tests and production code). Issue: SPR-9051
- Loading branch information
Showing
4 changed files
with
392 additions
and
0 deletions.
There are no files selected for viewing
167 changes: 167 additions & 0 deletions
167
...framework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/* | ||
* Copyright 2002-2012 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.junit4.spr9051; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertNotNull; | ||
import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; | ||
import static org.springframework.test.transaction.TransactionTestUtils.inTransaction; | ||
|
||
import javax.sql.DataSource; | ||
|
||
import org.junit.After; | ||
import org.junit.AfterClass; | ||
import org.junit.Before; | ||
import org.junit.BeforeClass; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.springframework.beans.Employee; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.jdbc.core.JdbcTemplate; | ||
import org.springframework.jdbc.datasource.DataSourceTransactionManager; | ||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | ||
import org.springframework.test.context.transaction.AfterTransaction; | ||
import org.springframework.test.context.transaction.BeforeTransaction; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
/** | ||
* This set of tests investigates the claims made in | ||
* <a href="https://jira.springsource.org/browse/SPR-9051" target="_blank">SPR-9051</a> | ||
* with regard to transactional tests. | ||
* | ||
* @author Sam Brannen | ||
* @since 3.2 | ||
* @see org.springframework.test.context.testng.AnnotationConfigTransactionalTestNGSpringContextTests | ||
*/ | ||
@RunWith(SpringJUnit4ClassRunner.class) | ||
public abstract class AbstractTransactionalAnnotatedConfigClassTests { | ||
|
||
protected static final String JANE = "jane"; | ||
protected static final String SUE = "sue"; | ||
protected static final String YODA = "yoda"; | ||
|
||
protected static final int NUM_TESTS = 2; | ||
protected static final int NUM_TX_TESTS = 1; | ||
|
||
private static int numSetUpCalls = 0; | ||
private static int numSetUpCallsInTransaction = 0; | ||
private static int numTearDownCalls = 0; | ||
private static int numTearDownCallsInTransaction = 0; | ||
|
||
protected DataSource dataSourceFromTxManager; | ||
protected DataSource dataSourceViaInjection; | ||
|
||
protected JdbcTemplate jdbcTemplate; | ||
|
||
@Autowired | ||
private Employee employee; | ||
|
||
|
||
@Autowired | ||
public void setTransactionManager(DataSourceTransactionManager transactionManager) { | ||
this.dataSourceFromTxManager = transactionManager.getDataSource(); | ||
} | ||
|
||
@Autowired | ||
public void setDataSource(DataSource dataSource) { | ||
this.dataSourceViaInjection = dataSource; | ||
this.jdbcTemplate = new JdbcTemplate(dataSource); | ||
} | ||
|
||
protected int countRowsInTable(String tableName) { | ||
return jdbcTemplate.queryForInt("SELECT COUNT(0) FROM " + tableName); | ||
} | ||
|
||
protected int createPerson(String name) { | ||
return jdbcTemplate.update("INSERT INTO person VALUES(?)", name); | ||
} | ||
|
||
protected int deletePerson(String name) { | ||
return jdbcTemplate.update("DELETE FROM person WHERE name=?", name); | ||
} | ||
|
||
protected void assertNumRowsInPersonTable(int expectedNumRows, String testState) { | ||
assertEquals("the number of rows in the person table (" + testState + ").", expectedNumRows, | ||
countRowsInTable("person")); | ||
} | ||
|
||
protected void assertAddPerson(final String name) { | ||
assertEquals("Adding '" + name + "'", 1, createPerson(name)); | ||
} | ||
|
||
@BeforeClass | ||
public static void beforeClass() { | ||
numSetUpCalls = 0; | ||
numSetUpCallsInTransaction = 0; | ||
numTearDownCalls = 0; | ||
numTearDownCallsInTransaction = 0; | ||
} | ||
|
||
@AfterClass | ||
public static void afterClass() { | ||
assertEquals("number of calls to setUp().", NUM_TESTS, numSetUpCalls); | ||
assertEquals("number of calls to setUp() within a transaction.", NUM_TX_TESTS, numSetUpCallsInTransaction); | ||
assertEquals("number of calls to tearDown().", NUM_TESTS, numTearDownCalls); | ||
assertEquals("number of calls to tearDown() within a transaction.", NUM_TX_TESTS, numTearDownCallsInTransaction); | ||
} | ||
|
||
@Test | ||
public void autowiringFromConfigClass() { | ||
assertNotNull("The employee should have been autowired.", employee); | ||
assertEquals("John Smith", employee.getName()); | ||
} | ||
|
||
@BeforeTransaction | ||
public void beforeTransaction() { | ||
assertNumRowsInPersonTable(0, "before a transactional test method"); | ||
assertAddPerson(YODA); | ||
} | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
numSetUpCalls++; | ||
if (inTransaction()) { | ||
numSetUpCallsInTransaction++; | ||
} | ||
assertNumRowsInPersonTable((inTransaction() ? 1 : 0), "before a test method"); | ||
} | ||
|
||
@Test | ||
@Transactional | ||
public void modifyTestDataWithinTransaction() { | ||
assertInTransaction(true); | ||
assertAddPerson(JANE); | ||
assertAddPerson(SUE); | ||
assertNumRowsInPersonTable(3, "in modifyTestDataWithinTransaction()"); | ||
} | ||
|
||
@After | ||
public void tearDown() throws Exception { | ||
numTearDownCalls++; | ||
if (inTransaction()) { | ||
numTearDownCallsInTransaction++; | ||
} | ||
assertNumRowsInPersonTable((inTransaction() ? 3 : 0), "after a test method"); | ||
} | ||
|
||
@AfterTransaction | ||
public void afterTransaction() { | ||
assertEquals("Deleting yoda", 1, deletePerson(YODA)); | ||
assertNumRowsInPersonTable(0, "after a transactional test method"); | ||
} | ||
|
||
} |
83 changes: 83 additions & 0 deletions
83
...est/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* | ||
* Copyright 2002-2012 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.junit4.spr9051; | ||
|
||
import static org.junit.Assert.assertSame; | ||
|
||
import javax.sql.DataSource; | ||
|
||
import org.junit.Before; | ||
import org.springframework.beans.Employee; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.jdbc.datasource.DataSourceTransactionManager; | ||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; | ||
import org.springframework.test.context.ContextConfiguration; | ||
import org.springframework.transaction.PlatformTransactionManager; | ||
|
||
/** | ||
* Concrete implementation of {@link AbstractTransactionalAnnotatedConfigClassTests} | ||
* that uses a true {@link Configuration @Configuration class}. | ||
* | ||
* @author Sam Brannen | ||
* @since 3.2 | ||
* @see TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests | ||
*/ | ||
@ContextConfiguration | ||
public class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends | ||
AbstractTransactionalAnnotatedConfigClassTests { | ||
|
||
/** | ||
* This is <b>intentionally</b> annotated with {@code @Configuration}. | ||
* | ||
* <p>Consequently, this class contains standard singleton bean methods | ||
* instead of <i>annotated factory bean methods</i>. | ||
*/ | ||
@Configuration | ||
static class Config { | ||
|
||
@Bean | ||
public Employee employee() { | ||
Employee employee = new Employee(); | ||
employee.setName("John Smith"); | ||
employee.setAge(42); | ||
employee.setCompany("Acme Widgets, Inc."); | ||
return employee; | ||
} | ||
|
||
@Bean | ||
public PlatformTransactionManager transactionManager() { | ||
return new DataSourceTransactionManager(dataSource()); | ||
} | ||
|
||
@Bean | ||
public DataSource dataSource() { | ||
return new EmbeddedDatabaseBuilder()// | ||
.addScript("classpath:/org/springframework/test/context/junit4/spr9051/schema.sql")// | ||
.build(); | ||
} | ||
|
||
} | ||
|
||
|
||
@Before | ||
public void compareDataSources() throws Exception { | ||
// NOTE: the two DataSource instances are the same! | ||
assertSame(dataSourceFromTxManager, dataSourceViaInjection); | ||
} | ||
|
||
} |
136 changes: 136 additions & 0 deletions
136
...ontext/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/* | ||
* Copyright 2002-2012 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.junit4.spr9051; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertNotSame; | ||
|
||
import javax.sql.DataSource; | ||
|
||
import org.junit.Before; | ||
import org.junit.runner.RunWith; | ||
import org.springframework.beans.Employee; | ||
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.context.ContextConfiguration; | ||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | ||
import org.springframework.test.context.transaction.AfterTransaction; | ||
import org.springframework.test.context.transaction.TransactionalTestExecutionListener; | ||
import org.springframework.transaction.PlatformTransactionManager; | ||
|
||
/** | ||
* Concrete implementation of {@link AbstractTransactionalAnnotatedConfigClassTests} | ||
* that does <b>not</b> use a true {@link Configuration @Configuration class} but | ||
* rather a <em>lite mode</em> configuration class (see the Javadoc for {@link Bean @Bean} | ||
* for details). | ||
* | ||
* @author Sam Brannen | ||
* @since 3.2 | ||
* @see Bean | ||
* @see TransactionalAnnotatedConfigClassWithAtConfigurationTests | ||
*/ | ||
@RunWith(SpringJUnit4ClassRunner.class) | ||
@ContextConfiguration(classes = TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class) | ||
public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests extends | ||
AbstractTransactionalAnnotatedConfigClassTests { | ||
|
||
/** | ||
* This is intentionally <b>not</b> annotated with {@code @Configuration}. | ||
* | ||
* <p>Consequently, this class contains <i>annotated factory bean methods</i> | ||
* instead of standard singleton bean methods. | ||
*/ | ||
// @Configuration | ||
static class AnnotatedFactoryBeans { | ||
|
||
@Bean | ||
public Employee employee() { | ||
Employee employee = new Employee(); | ||
employee.setName("John Smith"); | ||
employee.setAge(42); | ||
employee.setCompany("Acme Widgets, Inc."); | ||
return employee; | ||
} | ||
|
||
@Bean | ||
public PlatformTransactionManager transactionManager() { | ||
return new DataSourceTransactionManager(dataSource()); | ||
} | ||
|
||
/** | ||
* Since this method does not reside in a true {@code @Configuration class}, | ||
* it acts as a factory method instead of a singleton bean. The result is | ||
* that this method will be called at least twice: | ||
* | ||
* <ul> | ||
* <li>once <em>indirectly</em> by the {@link TransactionalTestExecutionListener} | ||
* when it retrieves the {@link PlatformTransactionManager} from the | ||
* application context</li> | ||
* <li>and again when the {@link DataSource} is injected into the test | ||
* instance in {@link AbstractTransactionalAnnotatedConfigClassTests#setDataSource(DataSource)}.</li> | ||
*</ul> | ||
* | ||
* Consequently, the {@link JdbcTemplate} used by this test instance and | ||
* the {@link PlatformTransactionManager} used by the Spring TestContext | ||
* Framework will operate on two different {@code DataSource} instances, | ||
* which is most certainly not the desired or intended behavior. | ||
*/ | ||
@Bean | ||
public DataSource dataSource() { | ||
return new EmbeddedDatabaseBuilder()// | ||
.addScript("classpath:/org/springframework/test/context/junit4/spr9051/schema.sql")// | ||
.build(); | ||
} | ||
|
||
} | ||
|
||
|
||
@Before | ||
public void compareDataSources() throws Exception { | ||
// NOTE: the two DataSource instances are NOT the same! | ||
assertNotSame(dataSourceFromTxManager, dataSourceViaInjection); | ||
} | ||
|
||
/** | ||
* Overrides {@code afterTransaction()} in order to assert a different result. | ||
* | ||
* <p>See in-line comments for details. | ||
* | ||
* @see AbstractTransactionalAnnotatedConfigClassTests#afterTransaction() | ||
* @see AbstractTransactionalAnnotatedConfigClassTests#modifyTestDataWithinTransaction() | ||
*/ | ||
@AfterTransaction | ||
@Override | ||
public void afterTransaction() { | ||
assertEquals("Deleting yoda", 1, deletePerson(YODA)); | ||
|
||
// NOTE: We would actually expect that there are now ZERO entries in the | ||
// person table, since the transaction is rolled back by the framework; | ||
// however, since our JdbcTemplate and the transaction manager used by | ||
// the Spring TestContext Framework use two different DataSource | ||
// instances, our insert statements were executed in transactions that | ||
// are not controlled by the test framework. Consequently, there was no | ||
// rollback for the two insert statements in | ||
// modifyTestDataWithinTransaction(). | ||
// | ||
assertNumRowsInPersonTable(2, "after a transactional test method"); | ||
} | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/schema.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
DROP TABLE person IF EXISTS; | ||
|
||
CREATE TABLE person ( | ||
name VARCHAR(20) NOT NULL, | ||
PRIMARY KEY(name) | ||
); |