I considered naming this “The Heisenberg Uncertainty Corollary for Java Exceptions”, but that was (a) too unwieldy, and (b) not sufficiently descriptive.
BLUF: I’m trying to catch, in a JUnit 5 test against a Spring Boot application, the exception thrown when a tuple is persisted to a database table with a constraint violation (duplicate value in column marked “unique”). I can catch the exception in try-catch block, but not using JUnit’s “assertThrows()”.
Elaboration
For ease-of-replication, I have narrowed down my code to only the entity and repository, and two tests (one works, the other is the reason for this post). Also for ease-of-replication, I am using H2 as the database.
I had read that there are potential transactional scope issues which can cause the constraint-generated exception to not be thrown within the scope of the invoking method. I confirmed this with a simple try-catch block around the statement “foos.aave(foo);” in shouldThrowExceptionOnSave() (without the “tem.flush()” statement).
I decided to use TestEntityManager.flush() to force the transaction to commit/end, and was able to successfully catch an exception in the try-catch block. However, it was not the expected DataIntegrityViolationException, but PersistenceException.
I attempted to use a similar mechanism (i.e., employ TestEntityManager.flush() to force the issue in the assertThrows() statement. But, “no joy”.
When I try “assertThrows(PersistenceException.class,…”, the method terminates with a DataIntegrityViolationException.
When I try “assertThrows(DataIntegrityViolationException.class,…”, I actually get a JUnit error message, indicating that the expected DataIntegrityViolationException didn’t match the actual exception. Which is…javax.persistence.PersistenceException!
Any help/insight would be greatly appreciated.
Add Note: The try-catch block in shouldThrowExceptionOnSave() is just to see what exception is caught.
Entity Class
package com.test.foo;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Foo {
@Id
@Column(name = "id",
nullable = false,
unique = true)
private String id;
@Column(name = "name",
nullable = false,
unique = true)
private String name;
public Foo() {
id = "Default ID";
name = "Default Name";
}
public Foo(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id;}
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
Repository Interface
package com.test.foo;
import org.springframework.data.repository.CrudRepository;
public interface FooRepository extends CrudRepository<Foo, String> { }
Repository Test Class
package com.test.foo;
import org.hibernate.exception.ConstraintViolationException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.dao.DataIntegrityViolationException;
import javax.persistence.PersistenceException;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DataJpaTest
public class FooRepositoryITest {
@Autowired
private TestEntityManager tem;
@Autowired
private FooRepository foos;
private static final int NUM_ROWS = 25;
private static final String BASE_ID = "->Test Id";
private static final String BASE_NAME = "->Test Name";
@BeforeEach
public void insertFooTuples() {
Foo foo;
for (int i=0; i<NUM_ROWS; i++) {
foo = new Foo(i+BASE_ID, i+BASE_NAME);
tem.persist(foo);
}
tem.flush();
}
@AfterEach
public void removeFooTuples() {
foos.findAll()
.forEach(tem::remove);
tem.flush();
}
@Test
public void shouldSaveNewTyple() {
Optional<Foo> newFoo;
String newId = "New Test Id";
String newName = "New Test Name";
Foo foo = new Foo(newId, newName);
foos.save(foo);
tem.flush();
newFoo = foos.findById(newId);
assertTrue(newFoo.isPresent(), "Failed to add Foo tuple");
}
@Test
public void shouldThrowExceptionOnSave() {
Optional<Foo> newFoo;
String newId = "New Test Id";
String newName = "New Test Name";
Foo foo = new Foo(newId, newName);
foo.setName(foos.findById(1+BASE_ID).get().getName());
try {
foos.save(foo);
tem.flush();
} catch(PersistenceException e) {
System.out.println("\n\n**** IN CATCH BLOCK ****\n\n");
System.out.println(e.toString());
}
// assertThrows(DataIntegrityViolationException.class,
// assertThrows(ConstraintViolationException.class,
assertThrows(PersistenceException.class,
() -> { foos.save(foo);
tem.flush();
} );
}
}
build.gradle
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'com.test'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
implementation('org.springframework.boot:spring-boot-starter-web')
runtimeOnly('com.h2database:h2')
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'junit'
exclude group: 'org.hamcrest'
}
testImplementation('org.junit.jupiter:junit-jupiter:5.4.0')
testImplementation('com.h2database:h2')
}
test {
useJUnitPlatform()
}
Output with "assertThrows(PersitenceException, ...)"
2019-02-25 14:55:12.747 WARN 15796 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23505, SQLState: 23505
2019-02-25 14:55:12.747 ERROR 15796 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Unique index or primary key violation: "UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('1->Test Name', 2)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]
**** IN CATCH BLOCK ****
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
.
. (some debug output removed for brevity)
.
2019-02-25 14:55:12.869 WARN 15796 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23505, SQLState: 23505
2019-02-25 14:55:12.869 ERROR 15796 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Unique index or primary key violation: "UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('1->Test Name', 2)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]
2019-02-25 14:55:12.877 INFO 15796 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@313ac989 testClass = FooRepositoryITest, testInstance = com.test.foo.FooRepositoryITest@71d44a3, testMethod = shouldThrowExceptionOnSave@FooRepositoryITest, testException = org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('1->Test Name', 2)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@4562e04d testClass = FooRepositoryITest, locations = '{}', classes = '{class com.test.foo.FooApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@527e5409, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@8b41920b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2a32de6c, [ImportsContextCustomizer@2a65fe7c key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@147ed70f, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@15b204a1, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('1->Test Name', 2)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
Output with "assertThrows(DataIntegrityViolationException, ...)
2019-02-25 14:52:16.880 WARN 2172 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23505, SQLState: 23505
2019-02-25 14:52:16.880 ERROR 2172 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Unique index or primary key violation: "UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('1->Test Name', 2)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]
**** IN CATCH BLOCK ****
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
.
. (some debug output removed for brevity)
.
insert into foo (name, id) values (?, ?) [23505-197]
2019-02-25 14:52:16.974 INFO 2172 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@313ac989 testClass = FooRepositoryITest, testInstance = com.test.foo.FooRepositoryITest@71d44a3, testMethod = shouldThrowExceptionOnSave@FooRepositoryITest, testException = org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <org.springframework.dao.DataIntegrityViolationException> but was: <javax.persistence.PersistenceException>, mergedContextConfiguration = [MergedContextConfiguration@4562e04d testClass = FooRepositoryITest, locations = '{}', classes = '{class com.test.foo.FooApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@527e5409, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@8b41920b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2a32de6c, [ImportsContextCustomizer@2a65fe7c key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@147ed70f, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@15b204a1, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]
org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==>
Expected :<org.springframework.dao.DataIntegrityViolationException>
Actual :<javax.persistence.PersistenceException>
<Click to see difference>
ConstraintViolationException. This is by far the most common cause of DataIntegrityViolationException being thrown – the Hibernate ConstraintViolationException indicates that the operation has violated a database integrity constraint.
Class DataIntegrityViolationException Exception thrown when an attempt to insert or update data results in violation of an integrity constraint. Note that this is not purely a relational concept; unique primary keys are required by most database types.
Environment : Probable Cause : This issue is caused when the requested dataset contains invalid data. In other words, some store records doesn't have valid data for one or more fields.
Your project actually does not use JUnit Jupiter 5.4. Rather, it's using JUnit Jupiter 5.3.2 as managed by Spring Boot. See Gradle 5 JUnit BOM and Spring Boot Incorrect Versions for the solution.
There is no need to flush()
in your @BeforeEach
method.
You should remove your @AfterEach
method since all changes to the database will be rolled back automatically with the test-managed transaction.
You actually cannot catch the ConstraintViolationException
since JPA will wrap it an a PersistenceException
, but you can verify that a ConstraintViolationException
caused the PersistenceException
.
To do that, simply rewrite your test as follows.
@Test
public void shouldThrowExceptionOnSave() {
String newId = "New Test Id";
String newName = "New Test Name";
Foo foo = new Foo(newId, newName);
foo.setName(fooRepository.findById(1 + BASE_ID).get().getName());
PersistenceException exception = assertThrows(PersistenceException.class, () -> {
fooRepository.save(foo);
testEntityManager.flush();
});
assertTrue(exception.getCause() instanceof ConstraintViolationException);
}
If you want to catch an exception from Spring's DataAccessException
hierarchy — such as DataIntegrityViolationException
, you have to ensure that the EntityManager#flush()
method is invoked in such a way that Spring performs exception translation.
Exception translation is performed via Spring's PersistenceExceptionTranslationPostProcessor
which wraps your @Repository
bean in a proxy in order to catch the exceptions and translate them. Spring Boot registers the PersistenceExceptionTranslationPostProcessor
for you automatically and ensures that your Spring Data JPA repositories are properly proxied.
In your example, you are invoking flush()
directly on Spring Boot's TestEntityManager
which does not perform exception translation. That is why you see the raw javax.persistence.PersistenceException
instead of Spring's DataIntegrityViolationException
.
If you want to assert that Spring will wrap the PersistenceException
in a DataIntegrityViolationException
, you need to do the following.
Redeclare your repository as follows. JpaRepository
gives you access to the flush()
method directly on your repository.
public interface FooRepository extends JpaRepository<Foo, String> {}
In your shouldThrowExceptionOnSave()
test method, invoke fooRepository.save(foo); fooRepository.flush();
or fooRepository.saveAndFlush(foo);
.
If you do so, the following will now pass.
@Test
public void shouldThrowExceptionOnSave() {
String newId = "New Test Id";
String newName = "New Test Name";
Foo foo = new Foo(newId, newName);
foo.setName(fooRepository.findById(1 + BASE_ID).get().getName());
assertThrows(DataIntegrityViolationException.class, () -> {
fooRepository.save(foo);
fooRepository.flush();
// fooRepository.saveAndFlush(foo);
});
}
Again, the reason this works is that the flush()
method is now invoked directly on your repository bean which Spring has wrapped in a proxy that catches the PersistenceException
and translates it into a DataIntegrityViolationException
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With