java - 如何使用 JUnit 5 在 Spring Boot 中捕获 Hibernate ConstraintViolationException(或 Spring DataIntegrityViolationException?)

标签 java spring spring-boot spring-data-jpa junit5

我考虑过将其命名为“Java 异常的海森堡不确定性推论”,但这 (a) 太笨拙,并且 (b) 描述不够充分。

BLUF:在针对 Spring Boot 应用程序的 JUnit 5 测试中,我试图捕捉当元组被持久化到数据库表时抛出的异常,该数据库表存在约束冲突(标记为“唯一”的列中的重复值)。我可以在 try-catch block 中捕获异常,但不能使用 JUnit 的“assertThrows()”。

阐述

为了便于复制,我将我的代码缩小到只有实体和存储库,以及两个测试(一个有效,另一个是这篇文章的原因)。同样为了便于复制,我使用 H2 作为数据库。

我读到过,存在潜在的事务范围问题,这些问题可能会导致约束生成的异常不会在调用方法的范围内抛出。我在语句“foos.aave(foo);”周围用一个简单的 try-catch block 证实了这一点。在 shouldThrowExceptionOnSave() 中(没有“tem.flush()”语句)。

我决定使用 TestEntityManager.flush() 强制事务提交/结束,并且能够在 try-catch block 中成功捕获异常。但是,这不是预期的 DataIntegrityViolationException,而是 PersistenceException。

我尝试使用类似的机制(即,使用 TestEntityManager.flush() 来强制 assertThrows() 语句中的问题。但是,“没有乐趣”。

当我尝试“assertThrows(PersistenceException.class,...”时,该方法以 DataIntegrityViolationException 终止。

当我尝试“assertThrows(DataIntegrityViolationException.class,...”时,我实际上收到了一条 JUnit 错误消息,表明预期的 DataIntegrityViolationException 与实际异常不匹配。这是...javax.persistence.PersistenceException!

任何帮助/见解将不胜感激。

补充说明:shouldThrowExceptionOnSave()中的try-catch block 只是为了看捕获到什么异常。

实体类

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> { }

存储库测试类

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();
                      } );
    }
}

构建.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。参见 Gradle 5 JUnit BOM and Spring Boot Incorrect Versions寻求解决方案。

没有必要flush()在你的@BeforeEach方法。

你应该删除你的 @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 存储库。

在您的示例中,您正在调用 flush()直接在 Spring Boot 的 TestEntityManager 上它不执行异常翻译。这就是为什么您看到原始 javax.persistence.PersistenceException 的原因而不是 Spring 的 DataIntegrityViolationException .

如果您想断言 Spring 将包装 PersistenceExceptionDataIntegrityViolationException ,您需要执行以下操作。

  1. 按如下方式重新声明您的存储库。 JpaRepository让您可以访问 flush()方法直接在您的存储库上。

    public interface FooRepository extends JpaRepository<Foo, String> {}

  2. 在你的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 .

关于java - 如何使用 JUnit 5 在 Spring Boot 中捕获 Hibernate ConstraintViolationException(或 Spring DataIntegrityViolationException?),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54876448/

相关文章:

java - 正则表达式从字符串中删除非数字和非小数点

java - 分离链接方法调用

java - Spring OAuth2 资源和授权服务器

spring - 自定义 spring 验证错误

java - Jackson 何时需要无参数构造函数进行反序列化?

java - 意外的 EOF;期待元素 <attribute> 的关闭标记

Spring i18n : problem with multiple property files

java - hibernate jdpa mysql 另一个列表查询中的任何列表条目

java - 如何在 Spring Boot 中将属性设置为通用 @Service 类?

java - 在嵌入式 tomcat 中以 ROOT 身​​份部署