Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you test Spring @Transactional without just hitting hibernate level 1 cache or doing manual session flush?

Using Spring + Hibernate and transactional annotations.

I'm trying to test the following:

  1. call a method that changes a User object then calls a @Transactional service method to persist it
  2. read the object back from the DB and insure it's values are correct after the method

The first problem I had was reading the User object in step 2 just returned the one in the Hibernate level 1 cache and did not actually read from the database.

I therefore manually evicted the object from the cache using the Session to force a read from the database. However, when I do this, the object values are never persisted within the unit test (I know it rolls back after the test is complete because of the settings I specified).

I tried manually flushing the session after the call to the @Transactional service method, and that DID commit the changes. However, that was not what I expected. I thought that a @Transactional service method would insure the transaction was committed and session flushed before it returned. I know in general Spring will decide when to do this management, but I thought the "unit of work" in a @Transactional method was that method.

In any case, now I'm trying to figure out how I would test a @Transactional method in general.

Here's a junit test method that is failing:

@RunWith(SpringJUnit4ClassRunner.class)
@Transactional
@TransactionConfiguration(transactionManager = "userTransactionManager", defaultRollback = true)
@WebAppConfiguration()
@ContextConfiguration(locations = { "classpath:test-applicationContext.xml",
        "classpath:test-spring-servlet.xml",
        "classpath:test-applicationContext-security.xml" })
public class HibernateTest {

    @Autowired
    @Qualifier("userSessionFactory")
    private SessionFactory sessionFactory;

    @Autowired
    private UserService userService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private QueryService queryService;

    @Autowired
    private NodeService nodeService;

    @Autowired
    private UserUtils userUtils;

    @Autowired
    private UserContext userContext;

  @Test
    public void testTransactions() {
        // read the user
        User user1 = userService.readUser(new Long(77));
        // change the display name
        user1.setDisplayName("somethingNew");
        // update the user using service method that is marked @Transactional
        userService.updateUserSamePassword(user1);
        // when I manually flush the session everything works, suggesting the
        // @Transactional has not flushed it at the end of the method marked
        // @Transactional, which implies it is leaving the transaction open?
        // session.flush();
        // evict the user from hibernate level 1 cache to insure we are reading
        // raw from the database on next read
        sessionFactory.getCurrentSession().evict(user1);
        // try to read the user again
        User user2 = userService.readUser(new Long(77));
        System.out.println("user1 displayName is " + user1.getDisplayName());
        System.out.println("user2 displayName is " + user2.getDisplayName());
        assertEquals(user1.getDisplayName(), user2.getDisplayName());
    }
}

If I manually flush the session, then the test succeeds. However, I would have expected the @Transactional method to take care of committing and flushing the session.

The service method for updateUserSamePassword is here:

@Transactional("userTransactionManager")
@Override
public void updateUserSamePassword(User user) {
    userDAO.updateUser(user);
}

The DAO method is here:

@Override
public void updateUser(User user) {
    Session session = sessionFactory.getCurrentSession();
    session.update(user);
}

SesssionFactory is autowired:

@Autowired
@Qualifier("userSessionFactory")
private SessionFactory sessionFactory;

I'm using XML application context configuration. I have:

<context:annotation-config />
<tx:annotation-driven transaction-manager="userTransactionManager" />

And

<bean id="userDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> 
    <property name="driverClass" value="${user.jdbc.driverClass}"/>
    <property name="jdbcUrl" value="${user.jdbc.jdbcUrl}" />
    <property name="user" value="${user.jdbc.user}" />
    <property name="password" value="${user.jdbc.password}" />
    <property name="initialPoolSize" value="3" />
    <property name="minPoolSize" value="1" />
    <property name="maxPoolSize" value="17" />
</bean>

<bean id="userSessionFactory"
    class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="userDataSource" />
    <property name="configLocation" value="classpath:user.hibernate.cfg.xml" />
</bean>

<bean id="userTransactionManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="dataSource" ref="userDataSource" />
    <property name="sessionFactory" ref="userSessionFactory" />
</bean>

There is also a component scan on the services and dao classes. As I said, this is working in production.

I thought that if I have a method marked @Transactional that by the end of that method (e.g. the update method here), Spring would have forced the Session to commit and flush.

I can see only a few options:

  1. I misconfigured something, even though this is working for me in general (just not the unit tests). Any guesses? Any ideas for how to test this?

  2. Something about the unit test config themselves is not behaving the way the app would.

  3. Transactions and sessions don't work like that. My only deduction is that Spring is leaving the transaction and/or the session open after calling that update method. So when I manually evict the user on the Session object, those changes haven't been committed yet.

Can anyone confirm if this is expected behavior? Shouldn't @Transaction have forced commit and flush on session? If not, then how would one test a method marked @Transactional and that the methods actually work with transactions?

I.e., how should I rewrite my Unit test here?

Any other ideas?

like image 957
user2166452 Avatar asked Oct 27 '14 21:10

user2166452


1 Answers

Here's what I was running into. Consider this code in a test method:

    String testDisplayNameChange = "ThisIsATest";
    User user = userService.readUser(new Long(77));
    user.setDisplayName(testDisplayNameChange);
    user = userService.readUser(new Long(77));
    assertNotEquals(user.getDisplayName(), testDisplayNameChange);

Note that the method userService.readUser is marked @Transactional in the service class.

If that test method is marked @Transactional the test fails. If it is NOT, it succeeds. Now I'm not sure if/when the Hibernate cache is actually getting involved. If the test method is transactional then each read happens in one transaction, and I believe they only hit the Hibernate level 1 cache (and don't actually read from the database). However, if the test method is NOT transactional, then each read happens in it's own transaction, and each does hit the database. Thus, the hibernate level 1 cache is tied to the session / transaction management.

Take aways:

  1. Even if a test method is calling multiple transactional methods in another class, if that test method itself is transactional, all of those calls happen in one transaction. The test method is the "unit of work". However, if the test method is NOT transactional, then each call to a transactional method within that test executes within it's own transaction.

  2. My test class was marked @Transactional, therefore every method will be transactional unless marked with an overriding annotation such as @AfterTransaction. I could just as easily NOT mark the class @Transactional and mark each method @Transactional

  3. Hibernate level 1 cache seems tied to the transaction when using Spring @Transactional. I.e. subsequent reads of an object within the same transaction will hit the hibernate level 1 cache and not the database. Note there is a level 2 cache and other mechanisms that you can tweak.

I was going to have a @Transactional test method then use @AfterTransaction on another method in the test class and submit raw SQL to evaluate values in the database. This would circumvent the ORM and hibernate level 1 cache altogether, insuring you were comparing actual values in the DB.

The simple answer was to just take @Transactional off of my test class. Yay.

like image 141
user2166452 Avatar answered Nov 11 '22 18:11

user2166452