Skip to content

Commit

Permalink
Investigate claims made in SPR-9051 regarding transactional tests
Browse files Browse the repository at this point in the history
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
sbrannen committed May 15, 2012
1 parent 9c223c1 commit 1cec0f9
Show file tree
Hide file tree
Showing 4 changed files with 392 additions and 0 deletions.
@@ -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");
}

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

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

}
@@ -0,0 +1,6 @@
DROP TABLE person IF EXISTS;

CREATE TABLE person (
name VARCHAR(20) NOT NULL,
PRIMARY KEY(name)
);

0 comments on commit 1cec0f9

Please sign in to comment.