How to rollback all changes in a JUnit test?

1,382 views
Skip to first unread message

Christofer Jennings

unread,
Jul 19, 2011, 12:18:35 AM7/19/11
to mybati...@googlegroups.com

Hi All,


This may be a nube question, as I'm a MyBatis nube :) 


I want to have a JUnit test call a transaction-bound method on a 'repo' (aka DAO) class, then roll back the changes after the test. That is, I want my whole test method to be in a transaction so it's side affects never commit. 


My app is set up to use Guice, so I'm using the @Transactional annotation from mybatis-guice to wrap multiple calls to a Mapper. Like so….


/* ===== from my 'repo' class ============*/

@Override

@Transactional(

      executorType = ExecutorType.REUSE,

      isolationLevel = TransactionIsolationLevel.REPEATABLE_READ,

      exceptionMessage = "Something went wrong while updating ECU {0}"

 )

 public Integer addUser(User user) {

    if (user != null && userMapper.addUser(user) > 0) {

       userMapper.deleteUserProducts(user); 

       if (user.getProducts() != null && !user.getProducts().isEmpty()) {

          userMapper.addUserProducts(data);

       }

       return user.getUserInstanceId();

    } else {

       return null;

    }

}


I based my test on SimpleBasicTest : http://code.google.com/p/mybatis/source/browse/sub-projects/mybatis-guice/trunk/src/test/java/org/mybatis/guice/sample/SampleBasicTest.java?r=3712

But it drops and creates the DB each setup, which is kinda slow.

I've tried a few tricks but haven't found


/* ===== test class ============*/

public class MyTest {


   private Injector injector;


   private UsersRepo usersRepo;


   @Before

   public void setup() throws Exception {

      // bindings

      this.injector = createInjector(new MyBatisModule() {


                  @Override

                  protected void initialize() {

                      install(JdbcHelper.MySQL);


                      bindDataSourceProviderType(PooledDataSourceProvider.class);

                      bindTransactionFactoryType(JdbcTransactionFactory.class);

                      

                      addMapperClass(UserMapper.class);


                      bindProperties(binder(), createTestProperties());

                      bind(UsersRepo.class).to(UsersRepoMyBatis.class);

                  }

              }

      );


      this.usersRepo = this.injector.getInstance(UsersRepo.class);

   }


   protected static Properties createTestProperties() {

       Properties props = new Properties();

       props.setProperty("mybatis.environment.id", "test");

       props.setProperty("JDBC.host", "localhost");

       props.setProperty("JDBC.port", "3306");

       props.setProperty("JDBC.schema", "myapp_test");

       props.setProperty("JDBC.username", "myapp_test");

       props.setProperty("JDBC.password", "changeme");

       props.setProperty("JDBC.autoCommit", "false");

       return props;

   }

   

   /** I'd like this whole test to be in a transaction that doesn't commit. */ 

   @Test

   public void testIt() throws Exception {

      // Want to start transaction here… via annotation, code, whatever

      assertEquals(0, usersRepo.getAllUsers().size());

      

      User user = new User();

      user.setName("Mark Twain");

      user.setProducts(null);

      

      int r = usersRepo.addUser(user);

      

      assertEquals(1, r);

      assertEquals(0, usersRepo.getAllUsers().size());

      // … and end transaction here with nothing committed.

      // But since there's no transaction the test only passes once. :(

   }

}


Many thanks in advance for any guidance.

,boz


Poitras Christian

unread,
Jul 19, 2011, 9:21:48 AM7/19/11
to mybati...@googlegroups.com
Oups... wrong Thread...

-----Message d'origine-----
De : mybati...@googlegroups.com [mailto:mybati...@googlegroups.com] De la part de Poitras Christian
Envoyé : July-19-11 9:20 AM
À : 'mybati...@googlegroups.com'
Objet : RE: Noobie questions - inability to share SqlSessions, etc

This code is not optimal, but it should get you started.

To use the Runner, simply add a @RunWith annotation on your test class like this:
@RunWith(ServiceTestRollbackRunner.class)

Be aware that @Before and @After will run outside the transaction scope.

The DatabaseCleaner class in ServiceTestRollbackRunner is used to bring the database to a known state before any tests are run.

Christian

-----Message d'origine-----
De : mybati...@googlegroups.com [mailto:mybati...@googlegroups.com] De la part de Craig Envoyé : July-19-11 2:31 AM À : mybatis-user Objet : Noobie questions - inability to share SqlSessions, etc

Hi,

I have a noobie question - answers or refs to previous threads much
appreciated:

The 3.0 doco states that SqlSessions should not be shared (between threads). Is this strictly true (due to some internal usage of thread locals perhaps), or is sharing just frowned upon since SqlSessions are not thread-safe?

Ideally, I would like to be able to use different threads at different times (i.e mutual exclusion) to invoke actions on a SqlSession, potentially even within a single database transaction...

ClearSqlSessionCacheInterceptor.java
RollbackStatement.java
ServiceTestRollbackRunner.java

Christofer Jennings

unread,
Jul 19, 2011, 2:48:44 PM7/19/11
to mybati...@googlegroups.com
Thanks Christian!

I get the basic idea of your code (have the runner wrap calls to test methods to manage the rollback) but am having trouble getting it to run. 

1) I don't understand how to configure Guice so it can create the SqlSessionManager. (see error log below) How does the SqlSessionManager get injected into the ServiceTestRollbackRunner? Do I need another guice configuration somewhere?

2) Does the line ...
   bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class), clearSqlSessionCacheInterceptor);
mean that I need to add a @Transactional annotation to my test method? (or is it binding based on my 'repo' method's annotation?)

====
com.google.inject.CreationException: Guice creation errors:

Could not find a suitable constructor in org.apache.ibatis.session.SqlSessionManager. Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
  at org.apache.ibatis.session.SqlSessionManager.class(SqlSessionManager.java:15)
  while locating org.apache.ibatis.session.SqlSessionManager
    for parameter 0 at myapp.dal.mybatis.ext.ClearSqlSessionCacheInterceptor.setSqlSessionManager(ClearSqlSessionCacheInterceptor.java:25)
  at myapp.dal.mybatis.ext.ClearSqlSessionCacheInterceptor.setSqlSessionManager(ClearSqlSessionCacheInterceptor.java:25)
  at myapp.dal.mybatis.ext.ServiceTestRollbackRunner$1.configure(ServiceTestRollbackRunner.java:38)
====

Thanks again for the help!
,boz

Poitras Christian

unread,
Jul 19, 2011, 3:50:46 PM7/19/11
to mybati...@googlegroups.com

Hi,

 

1)

The code I supplied was incomplete.

Obviously, you need to add your modules to the line:

injector = Guice.createInjector(

Once the injector has been created, your test class dependencies will be injected automatically by the Runner.

 

2)

The binding done on @Transactional is used to flush the cache.

Test case example:

1)      Select a value – value is put in cache

2)      Update this value

3)      Select same value

At step 3, you would get the old value if the cache is not flushed. So that’s why the interceptor will flush the cache each time it finds @Transactional.

 

If you use JDBC transactions, make sure you use @Transactional instead of sqlSession.commit() in your DAOs or the transaction will be committed and you will lose all advantages.

A workaround would be to use managed transactions.

 

Christian

 

De : mybati...@googlegroups.com [mailto:mybati...@googlegroups.com] De la part de Christofer Jennings
Envoyé : July-19-11 2:49 PM
À : mybati...@googlegroups.com
Objet : Re: RE: How to rollback all changes in a JUnit test?

Christofer Jennings

unread,
Jul 21, 2011, 3:32:00 AM7/21/11
to mybati...@googlegroups.com
A little update:

I've been able to get the test runner approach to work but so far have to bind all my Mappers and DAOs ('repos') in the ServiceTestRollbackRunner . This isn't great, but at least I can see the transaction rollback working as hoped. I think it is more of an issue of my limited understanding of Guice at this point.

Just for kicks, My runner's ctor now looks like this...

   public ServiceTestRollbackRunner(final Class<?> clazz) throws InitializationError {

      super(clazz);

      injector = Guice.createInjector(

            new MyBatisModule() {

               @Override

               protected void initialize() {

                  install(JdbcHelper.MySQL);


                  bindDataSourceProviderType(PooledDataSourceProvider.class);

                  bindTransactionFactoryType(JdbcTransactionFactory.class);

                  Names.bindProperties(binder(), createTestProperties());


                  requestStaticInjection(clazz);

                  ClearSqlSessionCacheInterceptor clearSqlSessionCacheInterceptor = new

                        ClearSqlSessionCacheInterceptor();

                  requestInjection(clearSqlSessionCacheInterceptor);

                  bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class),

                        clearSqlSessionCacheInterceptor);


                  addMapperClass(UserMapper.class);

                  bind(UserRepo.class).to(UserRepoMyBatis.class);

               }

            }

            );

      injector.injectMembers(this);

   }


I see now that the line injector.injectMembers(this); is how the runner gets it's injections.

So far, for my setup, I don't need a DatabaseCleaner. I simply use SQL scripts to set up the database and rely on rollback to keep it clean. Maybe that won't hold water over the long term though.

Now I need to find a way to delegate adding mappers and binding DAOs to the test class ("clazz"). Again, I think this is a Guice question.

Thanks again to Christian. I'm learning a lot from your help.
,boz

Poitras Christian

unread,
Jul 21, 2011, 11:25:08 AM7/21/11
to mybati...@googlegroups.com
Hi,

Personally, I don't need to do some bindings in my test classes since I group my test classes that have the same Guice config.
So I have about 5 runners with 5 different configs.

If you do need to make some binding in your test class, you can create a child injector based on the Runner's injector.

Christian
________________________________________
De : mybati...@googlegroups.com [mybati...@googlegroups.com] de la part de Christofer Jennings [boz....@gmail.com]
Date d'envoi : jeudi 21 juillet 2011 03:32
À : mybati...@googlegroups.com
Objet : Re: RE: RE: How to rollback all changes in a JUnit test?

Christofer Jennings

unread,
Jul 22, 2011, 3:47:23 PM7/22/11
to mybati...@googlegroups.com
Attached what I came up with. Couldn't have done it without your help Christian. Hope I can buy you a beer some time.

From the class's javadoc....

Runs test methods within a MyBatis-controlled SQL transaction and rolls back on test completion (failed or success). Test class must also have @MapperClasses defined or an InitializationException will be thrown. If a DAO interface is used then the interface must have @ImplementedBy defined. A mybatis_test.properties file must be in the root your test's classpath.

Example:
 @RunWith(MyBatisTestRollbackRunner.class)
 @MapperClasses({UserMapper.class, AddressMapper.class})
 public class UserDaoTest {
    @Inject
    private UserDao userDao;
    
    @Test
    ...
 }
 
 @ImplementedBy(UserDaoMyBatis.class)
 public interface UserDao {
    ...
 }
 
 public class UserDaoMyBatis implements UserDao {
    @Inject
    private UserMapper userMapper;
    
    @Inject
    private AddressMapper addressMapper;
    
    ...
 }

 #mybatis_test.properties
 mybatis.environment.id=test
 JDBC.host=localhost
 JDBC.port=3306
 JDBC.schema=myschema_test
 JDBC.username=myschema_tester
 JDBC.password=password

MyBatisTestRollbackRunner.java

Christofer Jennings

unread,
Jul 22, 2011, 4:28:14 PM7/22/11
to mybati...@googlegroups.com
One shortcoming of MyBatisTestRollbackRunner is that it limits test classes injection to Mappers. I haven't figured out how to make a child injector based on the runner's injector yet. That would be much nicer.
,boz

boz

unread,
Jul 22, 2011, 5:06:52 PM7/22/11
to mybati...@googlegroups.com
Christian, I see now why you don't create the MyBatisModule in your test runner class. MyBatisModule's methods like addMapper are all protected so you can't pass the module instance to the test class and have the mappers added that way. Also, only one MyBatisModule can be created because the SqlSessionManager is created in AbstractMyBatisModule, and so creating a child which is also a MyBatisModule fails.

grrr

Back to the drawing board!
,boz

boz

unread,
Jul 22, 2011, 7:10:01 PM7/22/11
to mybati...@googlegroups.com
OK. I like this version better. The Runner delegates to the Test to make the MyBatisModule, then uses it as a child injector. All the configuration can now be done in the test, but the test must implement MyBatisModuleProvider. I made PooledJdbcMyBatisModule so configuration can be shared between tests, but it's really just what I need currently. It can be replaced with whatever someone else needs. Also, the DAO interfaces no longer need to declare @ImplementedBy.

New example from the runner's javadoc...
Example:
 @RunWith(MyBatisTestRollbackRunner.class)
 public class UserDaoTest implements MyBatisModuleProvider {
    @Inject
    private UserDao userDao;
    
    @Override
    public MyBatisModule getMyBatisModule() {
       return new PooledJdbcMyBatisModule() {
          @Override
          public void initializeForTest() {
             addMapperClass(UserMapper.class);
             addMapperClass(AddressMapper.class);
          }
       };
    }

    @Test
    ...
 }
 
 @ImplementedBy(UserDaoMyBatis.class)
 public interface UserDao {
    ...
 }
 
 public class UserDaoMyBatis implements UserDao {
    @Inject
    private UserMapper userMapper;
    
    @Inject
    private AddressMapper addressMapper;
    
    ...
 }
 
MyBatisModuleProvider.java
MyBatisTestRollbackRunner.java
PooledJdbcMyBatisModule.java
Reply all
Reply to author
Forward
0 new messages