Skip to content

Commit 1cec0f9

Browse files
committed
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
1 parent 9c223c1 commit 1cec0f9

File tree

4 files changed

+392
-0
lines changed

4 files changed

+392
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.junit4.spr9051;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNotNull;
21+
import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction;
22+
import static org.springframework.test.transaction.TransactionTestUtils.inTransaction;
23+
24+
import javax.sql.DataSource;
25+
26+
import org.junit.After;
27+
import org.junit.AfterClass;
28+
import org.junit.Before;
29+
import org.junit.BeforeClass;
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
import org.springframework.beans.Employee;
33+
import org.springframework.beans.factory.annotation.Autowired;
34+
import org.springframework.jdbc.core.JdbcTemplate;
35+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
36+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
37+
import org.springframework.test.context.transaction.AfterTransaction;
38+
import org.springframework.test.context.transaction.BeforeTransaction;
39+
import org.springframework.transaction.annotation.Transactional;
40+
41+
/**
42+
* This set of tests investigates the claims made in
43+
* <a href="https://jira.springsource.org/browse/SPR-9051" target="_blank">SPR-9051</a>
44+
* with regard to transactional tests.
45+
*
46+
* @author Sam Brannen
47+
* @since 3.2
48+
* @see org.springframework.test.context.testng.AnnotationConfigTransactionalTestNGSpringContextTests
49+
*/
50+
@RunWith(SpringJUnit4ClassRunner.class)
51+
public abstract class AbstractTransactionalAnnotatedConfigClassTests {
52+
53+
protected static final String JANE = "jane";
54+
protected static final String SUE = "sue";
55+
protected static final String YODA = "yoda";
56+
57+
protected static final int NUM_TESTS = 2;
58+
protected static final int NUM_TX_TESTS = 1;
59+
60+
private static int numSetUpCalls = 0;
61+
private static int numSetUpCallsInTransaction = 0;
62+
private static int numTearDownCalls = 0;
63+
private static int numTearDownCallsInTransaction = 0;
64+
65+
protected DataSource dataSourceFromTxManager;
66+
protected DataSource dataSourceViaInjection;
67+
68+
protected JdbcTemplate jdbcTemplate;
69+
70+
@Autowired
71+
private Employee employee;
72+
73+
74+
@Autowired
75+
public void setTransactionManager(DataSourceTransactionManager transactionManager) {
76+
this.dataSourceFromTxManager = transactionManager.getDataSource();
77+
}
78+
79+
@Autowired
80+
public void setDataSource(DataSource dataSource) {
81+
this.dataSourceViaInjection = dataSource;
82+
this.jdbcTemplate = new JdbcTemplate(dataSource);
83+
}
84+
85+
protected int countRowsInTable(String tableName) {
86+
return jdbcTemplate.queryForInt("SELECT COUNT(0) FROM " + tableName);
87+
}
88+
89+
protected int createPerson(String name) {
90+
return jdbcTemplate.update("INSERT INTO person VALUES(?)", name);
91+
}
92+
93+
protected int deletePerson(String name) {
94+
return jdbcTemplate.update("DELETE FROM person WHERE name=?", name);
95+
}
96+
97+
protected void assertNumRowsInPersonTable(int expectedNumRows, String testState) {
98+
assertEquals("the number of rows in the person table (" + testState + ").", expectedNumRows,
99+
countRowsInTable("person"));
100+
}
101+
102+
protected void assertAddPerson(final String name) {
103+
assertEquals("Adding '" + name + "'", 1, createPerson(name));
104+
}
105+
106+
@BeforeClass
107+
public static void beforeClass() {
108+
numSetUpCalls = 0;
109+
numSetUpCallsInTransaction = 0;
110+
numTearDownCalls = 0;
111+
numTearDownCallsInTransaction = 0;
112+
}
113+
114+
@AfterClass
115+
public static void afterClass() {
116+
assertEquals("number of calls to setUp().", NUM_TESTS, numSetUpCalls);
117+
assertEquals("number of calls to setUp() within a transaction.", NUM_TX_TESTS, numSetUpCallsInTransaction);
118+
assertEquals("number of calls to tearDown().", NUM_TESTS, numTearDownCalls);
119+
assertEquals("number of calls to tearDown() within a transaction.", NUM_TX_TESTS, numTearDownCallsInTransaction);
120+
}
121+
122+
@Test
123+
public void autowiringFromConfigClass() {
124+
assertNotNull("The employee should have been autowired.", employee);
125+
assertEquals("John Smith", employee.getName());
126+
}
127+
128+
@BeforeTransaction
129+
public void beforeTransaction() {
130+
assertNumRowsInPersonTable(0, "before a transactional test method");
131+
assertAddPerson(YODA);
132+
}
133+
134+
@Before
135+
public void setUp() throws Exception {
136+
numSetUpCalls++;
137+
if (inTransaction()) {
138+
numSetUpCallsInTransaction++;
139+
}
140+
assertNumRowsInPersonTable((inTransaction() ? 1 : 0), "before a test method");
141+
}
142+
143+
@Test
144+
@Transactional
145+
public void modifyTestDataWithinTransaction() {
146+
assertInTransaction(true);
147+
assertAddPerson(JANE);
148+
assertAddPerson(SUE);
149+
assertNumRowsInPersonTable(3, "in modifyTestDataWithinTransaction()");
150+
}
151+
152+
@After
153+
public void tearDown() throws Exception {
154+
numTearDownCalls++;
155+
if (inTransaction()) {
156+
numTearDownCallsInTransaction++;
157+
}
158+
assertNumRowsInPersonTable((inTransaction() ? 3 : 0), "after a test method");
159+
}
160+
161+
@AfterTransaction
162+
public void afterTransaction() {
163+
assertEquals("Deleting yoda", 1, deletePerson(YODA));
164+
assertNumRowsInPersonTable(0, "after a transactional test method");
165+
}
166+
167+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.junit4.spr9051;
18+
19+
import static org.junit.Assert.assertSame;
20+
21+
import javax.sql.DataSource;
22+
23+
import org.junit.Before;
24+
import org.springframework.beans.Employee;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
28+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
29+
import org.springframework.test.context.ContextConfiguration;
30+
import org.springframework.transaction.PlatformTransactionManager;
31+
32+
/**
33+
* Concrete implementation of {@link AbstractTransactionalAnnotatedConfigClassTests}
34+
* that uses a true {@link Configuration @Configuration class}.
35+
*
36+
* @author Sam Brannen
37+
* @since 3.2
38+
* @see TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests
39+
*/
40+
@ContextConfiguration
41+
public class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends
42+
AbstractTransactionalAnnotatedConfigClassTests {
43+
44+
/**
45+
* This is <b>intentionally</b> annotated with {@code @Configuration}.
46+
*
47+
* <p>Consequently, this class contains standard singleton bean methods
48+
* instead of <i>annotated factory bean methods</i>.
49+
*/
50+
@Configuration
51+
static class Config {
52+
53+
@Bean
54+
public Employee employee() {
55+
Employee employee = new Employee();
56+
employee.setName("John Smith");
57+
employee.setAge(42);
58+
employee.setCompany("Acme Widgets, Inc.");
59+
return employee;
60+
}
61+
62+
@Bean
63+
public PlatformTransactionManager transactionManager() {
64+
return new DataSourceTransactionManager(dataSource());
65+
}
66+
67+
@Bean
68+
public DataSource dataSource() {
69+
return new EmbeddedDatabaseBuilder()//
70+
.addScript("classpath:/org/springframework/test/context/junit4/spr9051/schema.sql")//
71+
.build();
72+
}
73+
74+
}
75+
76+
77+
@Before
78+
public void compareDataSources() throws Exception {
79+
// NOTE: the two DataSource instances are the same!
80+
assertSame(dataSourceFromTxManager, dataSourceViaInjection);
81+
}
82+
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.junit4.spr9051;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNotSame;
21+
22+
import javax.sql.DataSource;
23+
24+
import org.junit.Before;
25+
import org.junit.runner.RunWith;
26+
import org.springframework.beans.Employee;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.jdbc.core.JdbcTemplate;
30+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
31+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
32+
import org.springframework.test.context.ContextConfiguration;
33+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
34+
import org.springframework.test.context.transaction.AfterTransaction;
35+
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
36+
import org.springframework.transaction.PlatformTransactionManager;
37+
38+
/**
39+
* Concrete implementation of {@link AbstractTransactionalAnnotatedConfigClassTests}
40+
* that does <b>not</b> use a true {@link Configuration @Configuration class} but
41+
* rather a <em>lite mode</em> configuration class (see the Javadoc for {@link Bean @Bean}
42+
* for details).
43+
*
44+
* @author Sam Brannen
45+
* @since 3.2
46+
* @see Bean
47+
* @see TransactionalAnnotatedConfigClassWithAtConfigurationTests
48+
*/
49+
@RunWith(SpringJUnit4ClassRunner.class)
50+
@ContextConfiguration(classes = TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class)
51+
public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests extends
52+
AbstractTransactionalAnnotatedConfigClassTests {
53+
54+
/**
55+
* This is intentionally <b>not</b> annotated with {@code @Configuration}.
56+
*
57+
* <p>Consequently, this class contains <i>annotated factory bean methods</i>
58+
* instead of standard singleton bean methods.
59+
*/
60+
// @Configuration
61+
static class AnnotatedFactoryBeans {
62+
63+
@Bean
64+
public Employee employee() {
65+
Employee employee = new Employee();
66+
employee.setName("John Smith");
67+
employee.setAge(42);
68+
employee.setCompany("Acme Widgets, Inc.");
69+
return employee;
70+
}
71+
72+
@Bean
73+
public PlatformTransactionManager transactionManager() {
74+
return new DataSourceTransactionManager(dataSource());
75+
}
76+
77+
/**
78+
* Since this method does not reside in a true {@code @Configuration class},
79+
* it acts as a factory method instead of a singleton bean. The result is
80+
* that this method will be called at least twice:
81+
*
82+
* <ul>
83+
* <li>once <em>indirectly</em> by the {@link TransactionalTestExecutionListener}
84+
* when it retrieves the {@link PlatformTransactionManager} from the
85+
* application context</li>
86+
* <li>and again when the {@link DataSource} is injected into the test
87+
* instance in {@link AbstractTransactionalAnnotatedConfigClassTests#setDataSource(DataSource)}.</li>
88+
*</ul>
89+
*
90+
* Consequently, the {@link JdbcTemplate} used by this test instance and
91+
* the {@link PlatformTransactionManager} used by the Spring TestContext
92+
* Framework will operate on two different {@code DataSource} instances,
93+
* which is most certainly not the desired or intended behavior.
94+
*/
95+
@Bean
96+
public DataSource dataSource() {
97+
return new EmbeddedDatabaseBuilder()//
98+
.addScript("classpath:/org/springframework/test/context/junit4/spr9051/schema.sql")//
99+
.build();
100+
}
101+
102+
}
103+
104+
105+
@Before
106+
public void compareDataSources() throws Exception {
107+
// NOTE: the two DataSource instances are NOT the same!
108+
assertNotSame(dataSourceFromTxManager, dataSourceViaInjection);
109+
}
110+
111+
/**
112+
* Overrides {@code afterTransaction()} in order to assert a different result.
113+
*
114+
* <p>See in-line comments for details.
115+
*
116+
* @see AbstractTransactionalAnnotatedConfigClassTests#afterTransaction()
117+
* @see AbstractTransactionalAnnotatedConfigClassTests#modifyTestDataWithinTransaction()
118+
*/
119+
@AfterTransaction
120+
@Override
121+
public void afterTransaction() {
122+
assertEquals("Deleting yoda", 1, deletePerson(YODA));
123+
124+
// NOTE: We would actually expect that there are now ZERO entries in the
125+
// person table, since the transaction is rolled back by the framework;
126+
// however, since our JdbcTemplate and the transaction manager used by
127+
// the Spring TestContext Framework use two different DataSource
128+
// instances, our insert statements were executed in transactions that
129+
// are not controlled by the test framework. Consequently, there was no
130+
// rollback for the two insert statements in
131+
// modifyTestDataWithinTransaction().
132+
//
133+
assertNumRowsInPersonTable(2, "after a transactional test method");
134+
}
135+
136+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
DROP TABLE person IF EXISTS;
2+
3+
CREATE TABLE person (
4+
name VARCHAR(20) NOT NULL,
5+
PRIMARY KEY(name)
6+
);

0 commit comments

Comments
 (0)