In our Spring application, we use a set of generic repository tests to validate our most basic use cases for using Spring repositories. One of those tests validates that an EntityNotFoundException is thrown if you try to get a previously deleted entity.
Before our migration to Spring Boot 3 and Hibernate 6, these tests worked correctly, but after the update, the expected exception was not thrown anymore.
You can reproduce this behavior with the following setup:
package de.test.hibernate_delete.repository;
import jakarta.persistence.*;
@jakarta.persistence.Entity
@Table(name = "entity")
public class Entity {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE) Long id;
@Version long version;
@Column(name = "data") String data;
public Entity(String data) {this.data = data;}
public Entity() {}
public Long getId() {return id;}
public void setId(Long id) {this.id = id;}
public long getVersion() {return version;}
public void setVersion(long version) {this.version = version;}
public String getData() {return data;}
public void setData(String data) {this.data = data;}
}
package de.test.hibernate_delete.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface EntityRepository extends JpaRepository<Entity, Long> {
}
package de.test.hibernate_delete;
import de.test.hibernate_delete.repository.Entity;
import de.test.hibernate_delete.repository.EntityRepository;
import jakarta.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = HibernateDeleteApplication.class)
public class EntityDeleteTest {
@Autowired private EntityRepository repository;
@Test public void testDelete() {
Entity storedSample = repository.saveAndFlush(new Entity("data"));
assertNotNull(storedSample.getId());
repository.delete(storedSample);
repository.flush();
assertThrows(EntityNotFoundException.class, () -> assertNull(repository.getOne(storedSample.getId())));
}
}
The test results in the following fail-message:
org.opentest4j.AssertionFailedError: Unexpected exception type thrown,
Expected :class jakarta.persistence.EntityNotFoundException
Actual :class org.opentest4j.AssertionFailedError
I have also pushed my minimal example to a github repo: https://github.com/LorenzSLA/hibernate_delete_minimal_exampel The failed test can be seen in this GitHub Actions Build: https://github.com/LorenzSLA/hibernate_delete_minimal_exampel/actions/runs/6094606983/job/16536481715
Has anyone had similar problems after migrating to Spring Boot 3?
Thanks to Andreys comment I changed the test case:
@Test
public void testDelete() {
T storedSample = getRepository().saveAndFlush(getSample());
assertNotNull(storedSample.getId());
getRepository().delete(storedSample);
getRepository().flush();
assertFalse(getRepository().existsById(storedSample.getId()));
}
That seems not to be related to Hibernate 6:
repository.getOne(storedSample.getId())
method returns proxy object, actually not backed up by live entity, so any interaction with such proxy object is expected to fail with EntityNotFoundException
, but the problem is your test case is actually checking junit assertions module.
Previous implementation of assertNull
method was trying to report assertion failure via:
String stringRepresentation = actual.toString();
if (stringRepresentation == null || stringRepresentation.equals("null")) {
fail(format(null, actual, message), null, actual);
} else {
fail(buildPrefix(message) + "expected: <null> but was: <" + actual + ">", null, actual);
}
where actual.toString()
call was throwing EntityNotFoundException
, now junit calls #toString()
method using more accurate way:
catch (Throwable throwable) {
UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
return defaultToString(obj);
}