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

2

14

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?

Slat answered 27/10, 2014 at 21:48 Comment(0)
S
10

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.

Slat answered 28/10, 2014 at 17:21 Comment(1)
very helpful explanation, but I'm still stubbornly trying to figure out how to refactor my similar test to not use @Transactional but still roll-back :(Toxoplasmosis
S
5

Q

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?

A

It is the expected behavior. The spring aware transactional unit test support by design rollsback the transaction after the test is finished. This is by design.

An implicit transaction boundary is created per test (each method with @Test) and once the test is finished a rollback is done.

The consequence of this is after all tests are finished there is no data that is actually changed. That is the goal is to be more "unit" like and less "integration" like. You should read the spring documentation as to why this is advantageous.

If you really want to test data being persisted and to view that data outside of the transaction boundary I recommend a more end-to-end test like a functional/integration test such as selenium or hitting your external WS/REST API if you have one.

Q

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

A

Your unit test does not work because you session.evict and not flush. Evicting will cause the changes not to be synchronized even though you already called session.update your in a transaction and hibernate batches operations. This is more clear with raw SQL as hibernate defers persisting to batch up all the operations so it waits until the session is flushed or closed to communicate with the database for performance. If you session.flush though the SQL will be executed right away (ie update will really happen) and then you can evict if you like to force a reread but your rereading within the transaction. I'm actually pretty sure flush will cause eviction so there is no need to call evict to force a reread but I might be wrong.

Sightless answered 28/10, 2014 at 1:44 Comment(16)
Actually it is exactly as mentioned. The transactional unit is the test method, so the commit (and thus flush) only would happen AFTER the end of the test method NOT the service method. For the test instead of evict you would have to flush the current session. Also in general for testing the values in the database you should use a simple jdbc statement not the same mechanism as you used to put it in.Through
That does not apply to what I'm describing. The data is not in the database DURING the test. The unit test I posted here (and description) should illustrate that clearly. Further, you can specify that behavior, as I have done here. But, just for kicks, now that you mention it, I'll turn that off so that it doesn't roll back and see what happens. It shouldn't have an effectSlat
Correct and it will never be because the transaction is started BEFORE the test method. Your test method is the transactional boundary NOT the service method. The commit will only happen when the transaction ends, it doesn't end at the call to your service method in the case of the test because the transaction was started before that.Through
setting rollback=false will not make a difference. The transactional boundary is still your test method. The only solution is to make your test not transactional to have the same behavior as in your web application.Through
Thanks M. Deinum, though I think your comment is orthogonal to the behavior of tests rolling back on completion. So the scope of the transaction is the entire test method that is calling the Transactional method? Is it generally the case that the scope of a transaction is the method calling a @Transactional method or is that particular to Transactional unit tests?Slat
o.k., that's fascinating - I'm going to make the test not TransactionalSlat
Now I can't get a handle on the Session to evict the from the Hibernate level 1 cache and force a reread of the object. This whole problem started because a test was succeeding when it should have failed because it was comparing values on an object it got from the level 1 cache (instead of reading it from the DB). I was trying to force it to read from the DB, but I guess you're saying to use straight up sql to test the values in the db. It seems there should be a way to have an ORM test "both ways", but whatever it takes I supposeSlat
A transaction is started as soon as a @Transactional is encountered in this case that is on the testcase (you put it there yourself!). The transaction ends at the same method that started the transaction every other method participates in that transaction (unless you have a REQUIRES_NEW or NEVER propagation level).Through
You shouldn't need an evict because you have a new session, the transaction also releases the session. The read and write should already happen on 2 different sessions.Through
btw - I think your answer is different from Adam's, and would suggest you post it as a separate answerSlat
I only need the Session to evict the object in order to force the next read to be from the database and not the hibernate level 1 cache. By default, even on @Transactional methods (and sessions are flushed and committed correctly), reads are often from the hibernate level 1 cache (which is on by default) and may never hit the database. The whole point of all of this is I got burned by a unit test that succeeded because it read from the cache when the values in the DB were incorrect. Test looked great, production code failed.Slat
@Slat You did ask: Can anyone confirm if this is expected behavior?. Here is the documentation that you should review: docs.spring.io/spring-framework/docs/3.2.x/…Sightless
@AdamGent, you are still misunderstanding the question. I have answered it. Yes, \@Transactional unit test methods default to rolling back after they complete unless you configure them not to. But my question is about testing a \@Transactional method, not a \@Transactional test method. NOT every test method is necessarily transactional unless the unit test class is marked transactional, as I have here. If I didn't, only test methods that are marked transactional are transactional. Any unit test, transactional or not, may test a transactional method of another class.Slat
The key to my issue was that even though I was calling multiple transactional methods from one test method, because the test method itself was transactional, all of those methods were executing in one transaction. M. Deinum pointed this out. After some experimentation, it appears that reads of an object within the same transaction default to the hibernate level 1 cache. It appears that as long as my test methods themselves are not transactional (and thus a different transaction is used for each transactional method called in one test method) that reads do hit the database.Slat
The key point being - I never expected the values to stay in the DB after the test method! I was writing and reading from the DB all within the same test method, and the values were not expected. So state rolling back after a test method is finished is not really relevant to my question.Slat
I'm sorry I just don't understand if you still have a problem. Perhaps answer the question with your own solution and mark it correct. I understand that your were confused with the fact that Spring will reuse the existing transaction (which was not clear at all from your original question). What I'm not sure is if you still have a problem.Sightless

© 2022 - 2024 — McMap. All rights reserved.