I'm trying to put together a bare-bones minimalist JUnit test for a system using Spring @Transactional annotations, and not having much success.
I'm creating two instances with the same value for a column that has a unique constraint. If the two instance creations happen to be in different transactions, I expect that the first will commit and the second will throw an exception, leading to one row -- and I see that happening. If the two inserts happen in the same Transaction, I expect both to be rolled back as an atomic unit, which I'm not seeing. I'm sure that there's a configuration issue somewhere, but I've not had much luck identifying it.
For the test I have a bean (ContextTestHelperImpl / ContextTestHelper) with methods to create either one or two instances. Each has a Propagation.REQUIRES_NEW annotation on the method:
@Override
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW,
rollbackFor = {ContextDuplicationException.class})
public Foo createOneFoo(int val) throws ContextDuplicationException {
try {
return fooDAO.createFoo(val);
} catch (Throwable th) {
throw new ContextDuplicationException(th);
}
}
@Override
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW,
rollbackFor = {ContextDuplicationException.class})
public Foo[] createTwoFoos(int val) throws ContextDuplicationException {
try {
return new Foo[] { fooDAO.createFoo(val), fooDAO.createFoo(val) };
} catch (Throwable th) {
throw new ContextDuplicationException(th);
}
}
I have one JUnit test (ContextTest) which calls the first method twice:
public void dupTestTwoTransactions() {
contextTestHelper.deleteAllFoo();
Assert.assertEquals(0, contextTestHelper.getCountOfFoo());
boolean hadException = false;
try {
contextTestHelper.createOneFoo(DUPLICATE_VALUE);
} catch (ContextDuplicationException th) {
hadException = true;
System.out.println("[ContextTest][dupTestTwoTransactions] UNEXPECTED ERROR: " + th);
th.printStackTrace();
}
Assert.assertEquals(hadException, false);
Assert.assertEquals(1, contextTestHelper.getCountOfFoo());
try {
contextTestHelper.createOneFoo(DUPLICATE_VALUE);
} catch (ContextDuplicationException th) {
hadException = true;
}
Assert.assertEquals(hadException, true);
Assert.assertEquals(1, contextTestHelper.getCountOfFoo());
}
This works as expected. The first call doesn't throw an exception; the second call does. At the end of it all, there's one row in the Foo table.
I have a second JUnit test in the same class which calls the latter method once:
@Test
public void dupTestOneTransaction() {
contextTestHelper.deleteAllFoo();
Assert.assertEquals(0, contextTestHelper.getCountOfFoo());
boolean hadException = false;
try {
contextTestHelper.createTwoFoos(DUPLICATE_VALUE);
} catch (ContextDuplicationException th) {
hadException = true;
}
Assert.assertEquals(hadException, true);
Assert.assertEquals(0, contextTestHelper.getCountOfFoo());
}
This second test fails on the final assertion -- the count of Foo instances is 1, while I'm expecting 0.
I have a little bit of shenanigans going on with the data source setup because we're trying to use a JNDI lookup for when the code runs under JBoss. As a result, JUnit needs to set up the JNDI lookup behind the scenes (ContextTest.java):
@BeforeClass
public static void setUpClass() throws NamingException {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-test.xml");
DataSource testDataSource = (DataSource) context.getBean("testDataSource");
SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
builder.bind("java:comp/env/jdbc/dataSource", testDataSource);
builder.activate();
}
Here is my spring-test.xml file which in NetBeans is set up in the Test Packages' default package:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:property-placeholder location="user-specific.properties"/>
<bean id="testDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName"><value>${db.driver.class}</value></property>
<property name="url"><value>${db.url}</value></property>
<property name="username"><value>${db.user}</value></property>
<property name="password"><value>${db.password}</value></property>
</bean>
</beans>
Since the first test works, I'm clearly able to connect to the database, so I don't think there's any particular problem here.
Here's applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd"
default-autowire="byName" >
<context:annotation-config />
<context:component-scan base-package="com.xyzzy" />
<tx:annotation-driven transaction-manager="transactionManager" />
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location">
<value>classpath:user-specific.properties</value>
</property>
</bean>
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/dataSource"/>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="fooDAO" class="com.xyzzy.FooDAOImpl" />
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<property name="packagesToScan" value="com.xyzzy" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">${db.dialect}</prop>
<prop key="hibernate.show_sql">${db.show_sql}</prop>
<prop key="hibernate.hbm2ddl.auto">${db.hbm2ddl.auto}</prop>
</props>
</property>
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="${db.dialect}"/>
<property name="generateDdl" value="true"/>
<property name="showSql" value="true"/>
</bean>
</property>
<property name="packagesToScan" value="com.xyzzy" />
</bean>
</beans>
All of the not-solely-for-testing classes (Foo, FooDAO, FooDAOImpl) are in the package com.xyzzy, and all of the testing ones (ContextTest, ContextTestHelper, ContextTestHelperImpl, ContextDuplicationException) are in com.xyzzy.test.
There are no @Transactional annotations on Foo, FooDAO or FooDAOImpl. ContextTestHelperImpl is the only one specifying Transaction boundaries.
Any suggestions for how to fix this so that it behaves as it should? (Is there something wrong with my choice of dataSource class, or transactionManager? Are some of my settings in applicationContext.xml inconsistent or redundant?)
UPDATE:
DAO implementation class:
@Repository("FooDAO")
public class FooDAOImpl implements FooDAO {
private HibernateTemplate hibernateTemplate;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
@Override
public Foo createFoo(int val) {
Foo foo = new Foo();
foo.setVal(val);
saveFoo(foo);
return foo;
}
void saveFoo(Foo foo) {
hibernateTemplate.save(foo);
}
[... and many similar methods ...]
I also tried it with Propagation.REQUIRED (read somewhere that it will cause an exception early on if there's not a Transaction associated with the thread), but that does not change its behavior.