Testing Hibernate Envers
Asked Answered
A

4

9

I would like to write tests about revision. In the console I see the update call of Hibernate, BUT no insertions into AUD-Table.

Test-Method:

@DataJpaTest
class JPAHistoryTest {

    @Test
    public void test() {
        def entity = // create Entity
        def entity2 = // create same Entity
        entity2.setPrice(entity2.getPrice() + 10)
        entity2.setLastUpdate(entity2.lastUpdate.plusSeconds(10))

        service.save(entity)
        service.save(entity2)
        repository.flush() // Hibernate updates changes

        assert repository.findRevisions(entity.id).content.empty == false // FAIL!
    }
}

My Entity looks like:

@Entity
@Audited
class Entity {
    @Id @GeneratedValue Long id
    @Column(nullable = false) BigDecimal price
}

Thank you very much.

Andorra answered 20/1, 2018 at 18:4 Comment(0)
A
11

As I found out I keep the @DataJpaTest and add @Transactional(propagation = NOT_SUPPORTED) to make sure, the test methods will not start a transaction. Because if they would run in a transaction, then the envers history entries will be written, when the test close the transaction.

@RunWith(SpringRunner)
@DataJpaTest
@Transactional(propagation = NOT_SUPPORTED)
class JPAHistoryTest {
    @After
    void after() {
        repository.deleteAll()
    }

    @Test
    public void testTwoInsert() {
        def entity1 = // ...
        def entity2 = // ...

        sut.save(entity1 )
        sut.save(entity2 )

        assert repository.findRevisions(entity1.id).content.size() == 1
        assert repository.findRevisions(entity2.id).content.size() == 1
    }
}
Andorra answered 21/1, 2018 at 10:48 Comment(0)
C
5

I found same problem as you and tried @michael-hegner answer, but I used TestEntityManager class so I can't get EntityManager when there is no transaction (propagation set to NOT_SUPPORTED).

In my case the solution was to manually commit transaction from TestEntityManager, to first save entity and changes and next query for revisions.

Here's a test class:

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private TestEntityManager testEntityManager;

    @Test
    public void shouldBeAudited() {
        User user = getTestUser();
        testEntityManager.persistAndFlush(user);

        user.setPassword("tset");
        testEntityManager.merge(user);

        // This line is critical here to commit transaction and trigger audit logs
        testEntityManager.getEntityManager().getTransaction().commit();

        // With propagation = NOT_SUPPORTED this doesn't work: testEntityManager.getEntityManager()
        AuditReader auditReader = AuditReaderFactory.get(testEntityManager.getEntityManager());
        List revisions = auditReader.createQuery()
                .forRevisionsOfEntity(User.class, true)
                .add(AuditEntity.id().eq(user.getId()))
                .getResultList();

        assertEquals(1, revisions.size());
    }

    private User getTestUser() {
        User user = new User();
        user.setUsername("test");
        user.setEmail("test");
        user.setPassword("test");
        return user;
    }

}

It could be mandatory to manually remove user after Test because transaction was commited and in other tests it may causes some problems.

Championship answered 1/3, 2020 at 11:0 Comment(1)
Thanks a lot. testEntityManager.getEntityManager().getTransaction().commit(); is a magic line and it made Envers to trigger audit queries.Marinara
V
2

If you don't want to change the transactional behaviour of the whole test class like in the answer of Michael Hegner, you can do the following:

@RunWith(SpringRunner)
@DataJpaTest
class JPAHistoryTest {

    @Autowired
    private MyRepository repository;
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    @After
    void after() {
        repository.deleteAll()
    }

    @Test
    public void testTwoInsert() {
        TransactionTemplate tx = new TransactionTemplate(platformTransactionManager);
        tx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);

        def entity1 = // ...
        def entity2 = // ...

        tx.execute(status -> repository.save(entity1))
        tx.execute(status -> repository.save(entity2))

        assert repository.findRevisions(entity1.id).content.size() == 1
        assert repository.findRevisions(entity2.id).content.size() == 1
    }
}

So create a TransactionTemplate with PROPAGATION_NOT_SUPPORTED and do the saving using that transaction template. This also triggers the auditing information being saved.

Vexation answered 6/4, 2021 at 13:54 Comment(0)
A
0

I honestly don't follow your naming convention here. Typically a service method call is wrapped by some transactional context in your production code.

@Service
public class Service1 {
  @Transactional
  public void save(Object object) {
  }
}

@Service
public class Service2 {
  @Transactional
  public void save(Object object) {
  }
}

For cases where you need to bind two service method calls within a single transactional context you'd wrap those two separate services by another service invocation.

@Service
public class ServiceWrapper {
  @Autowired
  private Service1 service1;
  @Autowired
  private Service2 service2;

  @Transactional
  public void save(Object object1, Object object2) {
    service1.save( object1 );
    service2.save( object2 );
  }
}

It's important to point out that this ServiceWrapper doesn't necessarily have to exist in your production code. This bean could be something you create solely for your test. The point is this bean is constructed in a very Spring-like way to satisfy the transaction boundary you need.

Now inside your test you can use the repository to lookup the entity because the transaction inside the wrapper commits the data, allowing the lookup to happen into the Envers audit tables as expected.

@Test
public void testSomething() {
  Object entity1 = //
  Object entity2 = //
  serviceWrapper.save( entity1, entity2 );

  // now use the repository to find the entity revision
}
Ardra answered 22/1, 2018 at 15:21 Comment(3)
Hi Naros, thanks your your reply, I will try that out.Andorra
Hi Naros, sorry for late reply. The point is, the data comes each by each through a Spring-Integration channel into the service, each one in its transaction. When it is done, the history entry will be written to the database, as I am aware when the transaction has been closed. The DataJpaTest includes Transactional, so my test method starts the transaction and will be closed after test case has been executed. But the history entries are not available yet which I want to validate. My final solution I will edit my answer above. Thank you.Andorra
We do something similar in our Envers test suite; however, what we have is a special annotation that forces a priority execution order of the @Test methods so that the data insertion method happens first and then our additional annotated test methods execute independently.Ardra

© 2022 - 2024 — McMap. All rights reserved.