Tuesday, July 17, 2007

Achieve Automatic Transaction Rollback with MockStrutsTestCase and Spring

Spring provides a convenience base class (AbstractTransactionalDataSourceSpringContextTests which extends the TestCase class from Junit) to automatically rollback any updates made to the database at the end of a test method. This works great when integration testing Service and DAO beans but it wont help when integration testing the Struts layer with MockStrutsTestCase.

Spring does not provide a class such as AbstractTransactionalDataSourceSpringContextTests that extends from the MockStrutsTestCase. This might be due to practical issues with the way Spring context initialization works when using MockStrutsTestCase. I tried achieving the transaction rollback in the same way as was done in the AbstractTransactionalDataSourceSpringContextTests class but it didn't work out as the application code started a new transaction and commited as usual without using the transaction I started in my own subclass of MockStrutsTestCase.

Another option left for me was to go down to the Transaction Manager level and achieve the rollbacks there. The application uses Hibernate at the DAO level and it was using HibernateTransactionManager as the transaction manager implementation when running test cases . Therefore I had to write a new class extending from HibernateTransactionManager which overrides the doCommit() method. The overridden method will call doRollback() method in the super class. This would rollback the current running transaction even if the declarative transaction handling code in Spring calls doCommit on the transaction manager.


package com.xyz.txn;

import org.springframework.orm.hibernate3.HibernateTransactionManager;

public class HibernateRollbackTxnManager extends HibernateTransactionManager {

/*
* doCommit method that calls the doRollback method of the super class.
*
*/
protected void doCommit(DefaultTransactionStatus txnStatus) {
super.doRollback(txnStatus);
}
}

Finally override the default transaction manager in your test spring bean configuration with the rollback only implementation.

<bean id="txnManager" class="com.com.xyz.txn.HibernateRollbackTxnManager">
<property name="sessionFactory"><ref local="sessionFactory"></property>
</bean>


The same strategy would work with other transaction managers such as DataSourceTransactionManager too.

Don' forget to click the +1 button below if this post was helpful.

6 comments:

Anonymous said...

I did try it, But no rollback is done at the end of the transaction.

don't know if there is some configuration to be done except what is in the content of the article.

Master of Nothing said...

I'm assuming you already have configured your service beans that are accessed by the action classes to be transactional.

Also make sure there are no other transaction managers configured in your spring files with a different name/id.

Finally add a System.out statement in the doCommit method to make sure Spring is using the HibernateRollbackTxnManager and calling it's doCommit method.

Anonymous said...

It is working now!

I had some transaction configuration issue

Thanks

Alex said...

Bull shit, man! You can acheve it without any custom transaction managers! Just extend you tests from the AbstractTransactional...Test and delegate processing of the requests to the struts mock container (StrutsTestCase).

Kashif said...

I tried it but I am facing the following exception.


servletunit.struts.ExceptionDuringTestError: An uncaught exception was thrown during actionExecute()
at servletunit.struts.MockStrutsTestCase.actionPerform(MockStrutsTestCase.java:409)
at com.nms.test.webapp.methodology.MilestoneListActionTest.executeAction(MilestoneListActionTest.java:35)
at com.nms.test.webapp.methodology.MilestoneListActionTest.testMilestoneList_submit(MilestoneListActionTest.java:56)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
------------
Root Cause:
------------
javax.servlet.ServletException: java.lang.IllegalStateException: Cannot deactivate transaction synchronization - not active
at org.apache.struts.action.RequestProcessor.processException(RequestProcessor.java:545)
at org.apache.struts.action.RequestProcessor.processActionPerform(RequestProcessor.java:486)
at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:274)
at org.apache.struts.action.ActionServlet.process(ActionServlet.java:1482)
at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:525)
at servletunit.struts.MockStrutsTestCase.actionPerform(MockStrutsTestCase.java:394)
at com.nms.test.webapp.methodology.MilestoneListActionTest.executeAction(MilestoneListActionTest.java:35)
at com.nms.test.webapp.methodology.MilestoneListActionTest.testMilestoneList_submit(MilestoneListActionTest.java:56)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
.
.
.
.

Kashif said...

Sorry, I was doing some mistake. I have cleared that and now it's working fine for me.