Integration testing with Hibernate Envers
Asked Answered
V

5

12

I'm trying to build some tests around some audited entities. My problem is that envers only audits on a transaction commit.

I need to create/edit some test objects, commit the transaction and then check the revisions.

What's the best approach to integration testing with envers?

Update: Here's a really bad, non-deterministic test class of what I want to achieve. I would prefer to do this without relying on the order of the test methods

First create an account and account_transaction in a single transaction. Both audited entries are for revision 1.

Second updated the account_transaction in a new transaction. The audited entry is at revision 2.

Third, load the audited account at revision 1 and do something with it.

@Transactional
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/testApplicationContext.xml"})
public class TestAuditing {

    @Autowired
    private AccountDao accountDao;

    @PersistenceContext
    private EntityManager entityManager;

    @Test
    @Rollback(false)
    public void first() {
        Account account = account("Test Account", "xxxxxxxx", "xxxxxx");

        AccountTransaction transaction = transaction(new Date(), Deposit, 100, "Deposit");
        account.setTransactions(newArrayList(transaction));

        accountDao.create(account);
    }

    @Test
    @Rollback(false)
    public void second() {
        Account account = accountDao.getById(1L);
        AccountTransaction transaction = account.getTransactions().get(0);
        transaction.setDescription("Updated Transaction");
        accountDao.update(account);
    }

    @Test
    public void third() {
        AuditReader reader = AuditReaderFactory.get(entityManager);

        List<Number> accountRevisions = reader.getRevisions(Account.class, 1L);
        //One revision [1]

        List<Number> transactionRevisions = reader.getRevisions(AccountTransaction.class, 1L);
        //Two revisions [1, 2]

        Account currentAccount = accountDao.getById(1L);
        Account revisionAccount = (Account) reader.createQuery().forEntitiesAtRevision(Account.class, 1).getSingleResult();

        System.out.println(revisionAccount);
    }
Vibratory answered 2/12, 2011 at 23:7 Comment(6)
Check this out - shameless self-promotion.Roar
Thanks for the reply Tomasz, I'm still not sure how to solve my problem from your blog post though. I don't really have a problem with the false positives from lazy loading etc, more with actually committing some transactions to setup some auditing test data. Maybe I missed something obvious in your post though?Vibratory
Well, scroll my article down to DbResetRule - my idea is to avoid using @Transactional JUnit tests and just let your code commit and rollback transactions. Obviously this makes tests non-repeatable and fragile. But instead of rolling back the changes I'm proposing dumping the database and restoring it before/after each test. Code is in Scala, but this is just a general idea. Let me know if this is what you are looking for so I will elaborate a little bit more in separate answer.Roar
I'm using an in memory database so the db reset isn't necessary. Doing the transactions manually would be brilliant except I don't know how. I get the following error for entityManager.getTransaction().begin(): Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT insteadVibratory
When you annotate method with @Transactional you don't have to do anything manually. Looks like you are already doing it. But when you remove @Transactional from test, you cannot simply use EntityManager in test. Either wrap its access in a transactional bean or use TransactionTemplate.Roar
Fantastic, just what I needed. Have added my code as an answer.Vibratory
V
5

As per Tomasz's suggestion I used a TransactionTemplate to achieve the commits after each dao operation. There is no class-level @Transactional annotation.

The envers audit entries are inserted before the method finishes which is just what I needed.

@ContextConfiguration("testApplicationContext.xml")
public class TestAuditing extends AbstractJUnit4SpringContextTests {

    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    @Autowired
    private PersonDao personDao;

    private TransactionTemplate template;

    @Before
    public void transactionTemplate() {
         template = new TransactionTemplate(platformTransactionManager);
    }

    @Test
    public void test() {
        Person person = createInTransaction(person("Karl", "Walsh", address("Middle of nowhere")), personDao);
        System.out.println(person);
    }

    private <T> T createInTransaction(final T object, final Dao<?, T> dao) {
        return template.execute(new TransactionCallback<T>() {
            public T doInTransaction(TransactionStatus transactionStatus) {
                dao.create(object);
                return object;
            }
        });
    }
}
Vibratory answered 3/12, 2011 at 20:4 Comment(0)
S
5

I am a user of Spring's transaction test support which rolls back tests when their done, and due to design of envers, revisions are not created. I created a hack that appears to allow one to "tell " envers to do its work, manually, before the transaction commits, but allows spring to continue to rollback.

These snippets should help. 1. Create your own auditlistener that overrides existing envers audit listener. This allows access to a static member visible to unit tests. There is probably a better way, but it works.

public class AuditEventListenerForUnitTesting extends AuditEventListener {

   public static AuditConfiguration auditConfig;

   @Override
   public void initialize(Configuration cfg) {
      super.initialize(cfg);
      auditConfig = super.getVerCfg();
   }
}

modify your persistence.xml to include this new listener class instead of one provided by envers

(repeat for other listeners if necessary)

Now within the "unit" test:

{
   saveNewList(owner); //code that does usual entity creation
   em.flush();  
   EventSource hibSession = (EventSource) em.getDelegate();
   AuditEventListenerForUnitTesting.auditConfig.getSyncManager().get(hibSession).doBeforeTransactionCompletion(hibSession);     
   //look for envers revisions now, and they should be there
}

I needed this because I have some JDBC queries against hibernate entities joined to the versioning tables.

Soinski answered 20/7, 2012 at 0:53 Comment(0)
V
2

This is strongly inspired by this previous answer adapted to work with Envers 4.2.19.Final (JPA 2.0). This solution does not need the transaction to commit either, which was a requirement in my case.

First create the following implementation of org.hibernate.integrator.spi.Integrator and add it to the classpath :

public class MyIntegrator implements Integrator {

  public static AuditConfiguration auditConfig;

  @Override
  public void integrate(Configuration configuration, SessionFactoryImplementor sessionFactory,
    SessionFactoryServiceRegistry serviceRegistry) {
    auditConfig = AuditConfiguration.getFor(configuration);
  }

  @Override
  public void integrate(MetadataImplementor metadata, SessionFactoryImplementor sessionFactory,
    SessionFactoryServiceRegistry serviceRegistry) {
    // NOP
  }

  @Override
  public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
    // NOP
  }

}

then create a META-INF/services/org.hibernate.integrator.spi.Integrator file under src/test/resources and paste the fully qulified name of the integrator class into it.

In your test, call your DAO method, flush the hibernate session and after it's done tell Envers to proceed :

EventSource es = (EventSource) entityManager.getDelegate();
SessionImplementor si = entityManager.unwrap(SessionImplementor.class);
MyIntegrator.auditConfig.getSyncManager().get(es).doBeforeTransactionCompletion(si);

you can then test the content of your DB and finally rollback the transaction.

Vicar answered 1/4, 2016 at 13:5 Comment(0)
W
1

Updated version of @thomas-naskali's answer for Envers 5.4.15.Final

    protected void flushEnvers()
    {
        final EventSource eventSource = entityManager.unwrap(EventSource.class);
        final Session session = entityManager.unwrap(Session.class);
        final SessionImplementor sessionImplementor = entityManager.unwrap(SessionImplementor.class);

        final EnversService service = session.getSessionFactory()
            .getSessionFactoryOptions()
            .getServiceRegistry()
            .getService(EnversService.class);

        service.getAuditProcessManager().get(eventSource)
            .doBeforeTransactionCompletion(sessionImplementor);
    }
Wet answered 19/5, 2020 at 10:38 Comment(1)
This! I needed to add session.flush() to get the behavior I needed, but this.Kunzite
P
-1

The two other solutions didn't worked for me, so I used another way, I just create a new transaction and force the commit. Everytime I need a new revision I do it again.

@Autowired
@Qualifier("transactionManager")
private PlatformTransactionManager platformTransactionManager;

@Test
public void enversTest() throws Exception{
    Entity myEntity = new Entity();

    TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
    myEntity.setName("oldName");
    myEntity = entityDao.merge(myEntity);
    platformTransactionManager.commit(status); // creates a revision with oldname

    TransactionStatus newStatus = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
    myEntity.setName("newName");
    myEntity = entityDao.merge(myEntity);
    platformTransactionManager.commit(newStatus); // creates a new revision with newName
}

If you use @Transactional(transactionManager="transactionManager") however it might bypass the commits and consider each test as one transaction (thus not versionning multiple time within the same test...)

Prussiate answered 30/3, 2016 at 8:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.