如何在 Spring Boot with JUnit 5 中捕获 Hibernate ConstraintViolationException(或 Spring DataIntegrityViolationException?)
How to catch Hibernate ConstraintViolationException (or Spring DataIntegrityViolationException?) in Spring Boot with JUnit 5
我考虑过将其命名为“Java 例外情况下的海森堡不确定性推论”,但这 (a) 太笨重,(b) 描述不够充分。
BLUF:在针对 Spring 启动应用程序的 JUnit 5 测试中,我试图捕获当元组持久保存到数据库 table 且违反约束时抛出的异常(列中标记为“唯一”的重复值)。我可以在 try-catch 块中捕获异常,但不能使用 JUnit 的“assertThrows()”。
详细说明
为了便于复制,我将我的代码缩小到只有实体和存储库,以及两个测试(一个有效,另一个是这个 post 的原因)。同样为了便于复制,我使用 H2 作为数据库。
我读到存在潜在的事务范围问题,这些问题可能导致约束生成的异常不在调用方法的范围内抛出。我在语句“foos.aave(foo);”周围用一个简单的 try-catch 块证实了这一点在 shouldThrowExceptionOnSave() 中(没有“tem.flush()”语句)。
我决定使用 TestEntityManager.flush() 强制事务到 commit/end,并且能够在 try-catch 块中成功捕获异常。但是,这不是预期的 DataIntegrityViolationException,而是 PersistenceException。
我尝试使用类似的机制(即,使用 TestEntityManager.flush() 来强制 assertThrows() 语句中的问题。但是,“不开心”。
当我尝试“assertThrows(PersistenceException.class,…”时,该方法以 DataIntegrityViolationException 终止。
当我尝试“assertThrows(DataIntegrityViolationException.class,…”时,我实际上收到了一条 JUnit 错误消息,表明预期的 DataIntegrityViolationException 与实际异常不匹配。这是…javax.persistence.PersistenceException!
任何 help/insight 将不胜感激。
添加注意:shouldThrowExceptionOnSave()中的try-catch块只是为了看捕获到什么异常。
实体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; }
}
存储库接口
package com.test.foo;
import org.springframework.data.repository.CrudRepository;
public interface FooRepository extends CrudRepository<Foo, String> { }
存储库测试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()
}
输出"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
输出为“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>
旁注
您的项目实际上没有使用 JUnit Jupiter 5.4。相反,它使用由 Spring Boot 管理的 JUnit Jupiter 5.3.2。请参阅 了解解决方案。
您的 @BeforeEach
方法中不需要 flush()
。
您应该删除 @AfterEach
方法,因为对数据库的所有更改都将通过测试管理的事务自动回滚。
捕获 ConstraintViolationException
您实际上无法捕捉到 ConstraintViolationException
,因为 JPA 会将其包装为 PersistenceException
,但您可以验证 ConstraintViolationException
导致了 PersistenceException
.
为此,只需按如下方式重写测试即可。
@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);
}
捕获 DataIntegrityViolationException
如果您想从 Spring 的 DataAccessException
层次结构中捕获异常 — 例如 DataIntegrityViolationException
,您必须确保 EntityManager#flush()
方法在Spring 执行异常转换的方式。
异常转换是通过 Spring 的 PersistenceExceptionTranslationPostProcessor
执行的,它将您的 @Repository
bean 包装在代理中,以便捕获异常并转换它们。 Spring Boot 会自动为您注册 PersistenceExceptionTranslationPostProcessor
并确保您的 Spring Data JPA 存储库被正确代理。
在您的示例中,您直接在 Spring Boot 的 TestEntityManager
上调用 flush()
,它不执行异常转换。这就是为什么您看到原始 javax.persistence.PersistenceException
而不是 Spring 的 DataIntegrityViolationException
.
如果您想断言 Spring 会将 PersistenceException
包装在 DataIntegrityViolationException
中,您需要执行以下操作。
按如下方式重新声明您的存储库。 JpaRepository
使您可以直接在存储库上访问 flush()
方法。
public interface FooRepository extends JpaRepository<Foo, String> {}
在您的 shouldThrowExceptionOnSave()
测试方法中,调用 fooRepository.save(foo); fooRepository.flush();
或 fooRepository.saveAndFlush(foo);
。
如果您这样做,以下内容现在将通过。
@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);
});
}
同样,这样做的原因是 flush()
方法现在直接在您的存储库 bean 上调用,Spring 已包装在捕获 PersistenceException
和 将翻译成DataIntegrityViolationException
。
感谢山姆的回答。有趣的是,Sam 的 "oh, by-the-way" 评论之一发现了我代码中的 "real issue"。
下面是最终代码(几乎),几乎可以工作。
"Almost",因为测试执行仍然失败,并且在尝试插入元组时似乎因违反约束而失败(请参阅下面的控制台日志片段)。
2019-02-27 09:28:50.237 INFO 4860 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: insert into foo (name, id) values (?, ?)
2019-02-27 09:28:50.311 WARN 4860 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23505, SQLState: 23505
2019-02-27 09:28:50.311 ERROR 4860 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Unique index or primary key violation: "UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('0->Test Name', 1)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]
2019-02-27 09:28:50.311 INFO 4860 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@1d296da testClass = FooRepositoryITest, testInstance = com.test.foo.FooRepositoryITest@6989da5e, 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 ('0->Test Name', 1)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@7c7a06ec 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@45018215, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@617263ed, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2f112965, [ImportsContextCustomizer@75d4a5c2 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@1f3f4916, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@59d016c9, 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 ('0->Test Name', 1)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
但是,这就是 "oh, by-the-way" 评论发挥作用的地方,它在实际测试中并没有失败。它似乎在 removeFooTuples() 中失败,它在 shouldThrowExceptionOnSave() 之后由 JUnit 执行。然而,即使 removeFooTuples() 只是试图从 table 中删除任何现有的元组(并且在异常不是 expected/thrown 的测试中这样做成功),控制台日志表明正在尝试 "insert"。
如果整个 removeFooTuples() 被注释掉,并且允许 JUnit 简单地删除 table,则测试成功执行到预期结果终止。我原以为 TestEntityManager.flush() 中的 shouldThrowExceptionOnSave() 会避免这种情况,但是...
@DataJpaTest
public class FooRepositoryITest {
@Autowired
private TestEntityManager tem;
@Autowired
private FooRepository foos;
private static final int NUM_ROWS = 1;
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();
}
/* shouldThrowExceptionOnSave() executes successfully if this
* method is commented out
*/
@AfterEach
public void removeFooTuples() {
foos.findAll()
.forEach(tem::remove);
tem.flush();
}
@Test
public void shouldThrowExceptionOnSave() {
String newId = "New Test Id";
String newName = "New Test Name";
Foo foo = new Foo(newId, newName);
foo.setName(foos.findById(0+BASE_ID).get().getName());
assertThrows(PersistenceException.class, () -> {
foos.save(foo);
tem.flush();
} );
}
}
我考虑过将其命名为“Java 例外情况下的海森堡不确定性推论”,但这 (a) 太笨重,(b) 描述不够充分。
BLUF:在针对 Spring 启动应用程序的 JUnit 5 测试中,我试图捕获当元组持久保存到数据库 table 且违反约束时抛出的异常(列中标记为“唯一”的重复值)。我可以在 try-catch 块中捕获异常,但不能使用 JUnit 的“assertThrows()”。
详细说明
为了便于复制,我将我的代码缩小到只有实体和存储库,以及两个测试(一个有效,另一个是这个 post 的原因)。同样为了便于复制,我使用 H2 作为数据库。
我读到存在潜在的事务范围问题,这些问题可能导致约束生成的异常不在调用方法的范围内抛出。我在语句“foos.aave(foo);”周围用一个简单的 try-catch 块证实了这一点在 shouldThrowExceptionOnSave() 中(没有“tem.flush()”语句)。
我决定使用 TestEntityManager.flush() 强制事务到 commit/end,并且能够在 try-catch 块中成功捕获异常。但是,这不是预期的 DataIntegrityViolationException,而是 PersistenceException。
我尝试使用类似的机制(即,使用 TestEntityManager.flush() 来强制 assertThrows() 语句中的问题。但是,“不开心”。
当我尝试“assertThrows(PersistenceException.class,…”时,该方法以 DataIntegrityViolationException 终止。
当我尝试“assertThrows(DataIntegrityViolationException.class,…”时,我实际上收到了一条 JUnit 错误消息,表明预期的 DataIntegrityViolationException 与实际异常不匹配。这是…javax.persistence.PersistenceException!
任何 help/insight 将不胜感激。
添加注意:shouldThrowExceptionOnSave()中的try-catch块只是为了看捕获到什么异常。
实体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; }
}
存储库接口
package com.test.foo;
import org.springframework.data.repository.CrudRepository;
public interface FooRepository extends CrudRepository<Foo, String> { }
存储库测试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()
}
输出"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
输出为“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>
旁注
您的项目实际上没有使用 JUnit Jupiter 5.4。相反,它使用由 Spring Boot 管理的 JUnit Jupiter 5.3.2。请参阅
您的 @BeforeEach
方法中不需要 flush()
。
您应该删除 @AfterEach
方法,因为对数据库的所有更改都将通过测试管理的事务自动回滚。
捕获 ConstraintViolationException
您实际上无法捕捉到 ConstraintViolationException
,因为 JPA 会将其包装为 PersistenceException
,但您可以验证 ConstraintViolationException
导致了 PersistenceException
.
为此,只需按如下方式重写测试即可。
@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);
}
捕获 DataIntegrityViolationException
如果您想从 Spring 的 DataAccessException
层次结构中捕获异常 — 例如 DataIntegrityViolationException
,您必须确保 EntityManager#flush()
方法在Spring 执行异常转换的方式。
异常转换是通过 Spring 的 PersistenceExceptionTranslationPostProcessor
执行的,它将您的 @Repository
bean 包装在代理中,以便捕获异常并转换它们。 Spring Boot 会自动为您注册 PersistenceExceptionTranslationPostProcessor
并确保您的 Spring Data JPA 存储库被正确代理。
在您的示例中,您直接在 Spring Boot 的 TestEntityManager
上调用 flush()
,它不执行异常转换。这就是为什么您看到原始 javax.persistence.PersistenceException
而不是 Spring 的 DataIntegrityViolationException
.
如果您想断言 Spring 会将 PersistenceException
包装在 DataIntegrityViolationException
中,您需要执行以下操作。
按如下方式重新声明您的存储库。
JpaRepository
使您可以直接在存储库上访问flush()
方法。public interface FooRepository extends JpaRepository<Foo, String> {}
在您的
shouldThrowExceptionOnSave()
测试方法中,调用fooRepository.save(foo); fooRepository.flush();
或fooRepository.saveAndFlush(foo);
。
如果您这样做,以下内容现在将通过。
@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);
});
}
同样,这样做的原因是 flush()
方法现在直接在您的存储库 bean 上调用,Spring 已包装在捕获 PersistenceException
和 将翻译成DataIntegrityViolationException
。
感谢山姆的回答。有趣的是,Sam 的 "oh, by-the-way" 评论之一发现了我代码中的 "real issue"。
下面是最终代码(几乎),几乎可以工作。
"Almost",因为测试执行仍然失败,并且在尝试插入元组时似乎因违反约束而失败(请参阅下面的控制台日志片段)。
2019-02-27 09:28:50.237 INFO 4860 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: insert into foo (name, id) values (?, ?)
2019-02-27 09:28:50.311 WARN 4860 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23505, SQLState: 23505
2019-02-27 09:28:50.311 ERROR 4860 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Unique index or primary key violation: "UK_A7S9IMMDPCXHLN2D4JHLAY516_INDEX_1 ON PUBLIC.FOO(NAME) VALUES ('0->Test Name', 1)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]
2019-02-27 09:28:50.311 INFO 4860 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@1d296da testClass = FooRepositoryITest, testInstance = com.test.foo.FooRepositoryITest@6989da5e, 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 ('0->Test Name', 1)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@7c7a06ec 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@45018215, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@617263ed, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2f112965, [ImportsContextCustomizer@75d4a5c2 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@1f3f4916, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@59d016c9, 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 ('0->Test Name', 1)"; SQL statement:
insert into foo (name, id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
但是,这就是 "oh, by-the-way" 评论发挥作用的地方,它在实际测试中并没有失败。它似乎在 removeFooTuples() 中失败,它在 shouldThrowExceptionOnSave() 之后由 JUnit 执行。然而,即使 removeFooTuples() 只是试图从 table 中删除任何现有的元组(并且在异常不是 expected/thrown 的测试中这样做成功),控制台日志表明正在尝试 "insert"。
如果整个 removeFooTuples() 被注释掉,并且允许 JUnit 简单地删除 table,则测试成功执行到预期结果终止。我原以为 TestEntityManager.flush() 中的 shouldThrowExceptionOnSave() 会避免这种情况,但是...
@DataJpaTest
public class FooRepositoryITest {
@Autowired
private TestEntityManager tem;
@Autowired
private FooRepository foos;
private static final int NUM_ROWS = 1;
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();
}
/* shouldThrowExceptionOnSave() executes successfully if this
* method is commented out
*/
@AfterEach
public void removeFooTuples() {
foos.findAll()
.forEach(tem::remove);
tem.flush();
}
@Test
public void shouldThrowExceptionOnSave() {
String newId = "New Test Id";
String newName = "New Test Name";
Foo foo = new Foo(newId, newName);
foo.setName(foos.findById(0+BASE_ID).get().getName());
assertThrows(PersistenceException.class, () -> {
foos.save(foo);
tem.flush();
} );
}
}