关于java:Testing-JPA-Queries-with-Spring-Boot-and-DataJpaTest

6次阅读

共计 8430 个字符,预计需要花费 22 分钟才能阅读完成。

【注】本文译自:Testing JPA Queries with Spring Boot and @DataJpaTest – Reflectoring

除了单元测试,集成测试在生产高质量的软件中起着至关重要的作用。一种非凡的集成测试解决咱们的代码和数据库之间的集成。
通过 @DataJpaTest 正文,Spring Boot 提供了一种便捷的办法来设置一个具备嵌入式数据库的环境,以测试咱们的数据库查问。
在本教程中,咱们将首先探讨哪些类型的查问值得测试,而后探讨创立用于测试的数据库模式和数据库状态的不同办法。

 代码示例

本文附有 GitHub 上的工作代码示例

依赖

在本教程中,除了通常的 Spring Boot 依赖项之外,咱们应用 JUnit Jupiter 作为咱们的测试框架,应用 H2 作为内存数据库。

dependencies {compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-web')
  runtime('com.h2database:h2')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('org.junit.jupiter:junit-jupiter-engine:5.2.0')
}

测试什么?

首先要答复咱们本人的问题是咱们须要测试什么。让咱们思考一个负责 UserEntity 对象的 Spring Data 存储库:

interface UserRepository extends CrudRepository<UserEntity, Long> {// query methods}

咱们有不同的选项来创立查问。让咱们具体看看其中的一些,以确定咱们是否应该用测试来笼罩它们。

推断查问

第一个选项是创立一个推断查问:

UserEntity findByName(String name);

咱们不须要通知 Spring Data 要做什么,因为它会主动从办法名称的名称推断 SQL 查问。
这个个性的益处是 Spring Data 还会在启动时主动查看查问是否无效。如果咱们将办法重命名为 findByFoo() 并且 UserEntity 没有属性 foo,Spring Data 会向咱们抛出一个异样来指出这一点:

org.springframework.data.mapping.PropertyReferenceException:
  No property foo found for type UserEntity!

因而,只有咱们至多有一个测试尝试在咱们的代码库中启动 Spring 应用程序上下文,咱们就不须要为咱们的推断查问编写额定的测试。

请留神,对于从 findByNameAndRegistrationDateBeforeAndEmailIsNotNull() 等长办法名称推断出的查问,状况并非如此。这个办法名很难把握,也很容易出错,所以咱们应该测试它是否真的合乎咱们的预期。

话虽如此,将此类办法重命名为更短、更有意义的名称并增加 @Query 正文以提供自定义 JPQL 查问是一种很好的做法。

应用 @Query 自定义 JPQL 查问

如果查问变得更简单,提供自定义 JPQL 查问是有意义的:

@Query("select u from UserEntity u where u.name = :name")
UserEntity findByNameCustomQuery(@Param("name") String name);

与推断查问相似,咱们能够收费对这些 JPQL 查问进行有效性查看。应用 Hibernate 作为咱们的 JPA 提供者,如果发现有效查问,咱们将在启动时失去一个 QuerySyntaxException

org.hibernate.hql.internal.ast.QuerySyntaxException:
unexpected token: foo near line 1, column 64 [select u from ...]

然而,自定义查问比通过单个属性查找条目要简单得多。例如,它们可能包含与其余表的连贯或返回简单的 DTO 而不是实体。
那么,咱们应该为自定义查问编写测试吗?令人不称心的答案是,咱们必须本人决定查问是否简单到须要测试。

应用 @Query 的本地查问

另一种办法是应用 本地查问

@Query(
  value = "select * from user as u where u.name = :name",
  nativeQuery = true)
UserEntity findByNameNativeQuery(@Param("name") String name);

咱们没有指定 JPQL 查问(它是对 SQL 的形象),而是间接指定一个 SQL 查问。此查问可能应用特定数据库的 SQL 方言。
须要留神的是,Hibernate 和 Spring Data 都不会在启动时验证本地查问 。因为查问可能蕴含特定于数据库的 SQL,因而 Spring Data 或 Hibernate 无奈晓得要查看什么。
因而,本地查问是集成测试的次要候选者。然而,如果他们真的应用特定数据库的 SQL,那么这些测试可能不适用于嵌入式内存数据库,因而咱们必须在后盾提供一个实在的数据库(比方,在继续集成管道中按需设置的 docker 容器中)。

@DataJpaTest 简介

为了测试 Spring Data JPA 存储库或任何其余与 JPA 相干的组件,Spring Boot 提供了 @DataJpaTest 注解。咱们能够将它增加到单元测试中,它将设置一个 Spring 应用程序上下文:

@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserEntityRepositoryTest {

  @Autowired private DataSource dataSource;
  @Autowired private JdbcTemplate jdbcTemplate;
  @Autowired private EntityManager entityManager;
  @Autowired private UserRepository userRepository;

  @Test
  void injectedComponentsAreNotNull(){assertThat(dataSource).isNotNull();
    assertThat(jdbcTemplate).isNotNull();
    assertThat(entityManager).isNotNull();
    assertThat(userRepository).isNotNull();}
}

@ExtendWith
本教程中的代码示例应用 @ExtendWith 注解通知 JUnit 5 启用 Spring 反对。从 Spring Boot 2.1 开始,咱们不再须要加载 SpringExtension,因为它作为元注解蕴含在 Spring Boot 测试注解中,例如 @DataJpaTest、@WebMvcTest 和 @SpringBootTest。本教程中的代码示例应用 @ExtendWith 注解通知 JUnit 5 启用 Spring 反对。从 Spring Boot 2.1 开始,咱们不再须要加载 SpringExtension,因为它作为元注解蕴含在 Spring Boot 测试注解中,例如 @DataJpaTest@WebMvcTest@SpringBootTest

这样创立的应用程序上下文将不蕴含咱们的 Spring Boot 应用程序所需的整个上下文,而只是它的一个“切片”,其中蕴含初始化任何 JPA 相干组件(如咱们的 Spring Data 存储库)所需的组件。
例如,如果须要,咱们能够将 DataSource@JdbcTemplate@EntityManage 注入咱们的测试类。此外,咱们能够从咱们的应用程序中注入任何 Spring Data 存储库。上述所有组件将主动配置为指向嵌入式内存数据库,而不是咱们可能在 application.propertiesapplication.yml 文件中配置的“实在”数据库。
请留神,默认状况下,蕴含所有这些组件(包含内存数据库)的应用程序上下文在所有 @DataJpaTest 注解的测试类中的所有测试方法之间共享。
这就是为什么在默认状况下 每个测试方法都在本人的事务中运行的起因,该事务在办法执行后回滚。这样,数据库状态在测试之间放弃原始状态,并且测试放弃互相独立。

创立数据库模式

在咱们能够测试对数据库的任何查问之前,咱们须要创立一个 SQL 模式来应用。让咱们看看一些不同的办法来做到这一点。

应用 Hibernate ddl-auto

默认状况下,@DataJpaTest 会配置 Hibernate 为咱们主动创立数据库模式。对此负责的属性是 spring.jpa.hibernate.ddl-auto,Spring Boot 默认将其设置为 create-drop,这意味着模式在运行测试之前创立并在测试执行后删除。
因而,如果咱们对 Hibernate 为咱们创立模式感到称心,咱们就不用做任何事件。

应用 schema.sql

Spring Boot 反对在应用程序启动时执行自定义 schema.sql 文件。
如果 Spring 在类门路中找到 schema.sql 文件,则将针对数据源执行该文件。这会笼罩下面探讨的 Hibernate 的 ddl-auto 配置。
咱们能够应用属性 spring.datasource.initialization-mode 管制是否应该执行 schema.sql。默认值是嵌入的,这意味着它只会对嵌入的数据库执行(即在咱们的测试中)。如果咱们将其设置为 always,它将始终执行。
以下日志输入确认文件已被执行:

Executing SQL script from URL [file:.../out/production/resources/schema.sql]

设置 Hibernate 的 ddl-auto 配置以在应用脚本初始化架构时进行验证是有意义的,以便 Hibernate 在启动时查看创立的模式是否与实体类匹配:

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {"spring.jpa.hibernate.ddl-auto=validate"})
class SchemaSqlTest {...}

应用 Flyway

Flyway 是一种数据库迁徙工具,容许指定多个 SQL 脚本来创立数据库模式。它会跟踪目标数据库上曾经执行了这些脚本中的哪些脚本,以便只执行之前没有执行过的脚本。
要激活 Flyway,咱们只须要将依赖项放入咱们的 build.gradle 文件中(如果咱们应用 Maven,则相似):

compile('org.flywaydb:flyway-core')

如果咱们没有专门配置 Hibernate 的 ddl-auto 配置,它会主动退出,因而 Flyway 具备优先权,并且默认状况下会针对咱们的内存数据库测试执行它在文件夹 src/main/resources/db/migration 中找到的所有 SQL 脚本。
同样,将 ddl-auto 设置为 validate 是有意义的,让 Hibernate 查看 Flyway 生成的模式是否合乎咱们的 Hibernate 实体的冀望:

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {"spring.jpa.hibernate.ddl-auto=validate"})
class FlywayTest {...}

在测试中应用 Flyway 的价值

如果咱们在生产中应用 Flyway,也能在下面形容的那样在 JPA 测试中应用它,那就太好了。只有这样咱们能力在测试时晓得 flyway 脚本按预期工作。
然而,这仅实用于脚本蕴含在生产数据库和测试中应用的内存数据库(咱们的示例中为 H2 数据库)上都无效的 SQL。如果不是这种状况,咱们必须在咱们的测试中禁用 Flyway,办法是将 spring.flyway.enabled 属性设置为 false,并将 spring.jpa.hibernate.ddl-auto 属性设置为 create-drop 以让 Hibernate 生成模式。
无论如何,让咱们确保将 ddl-auto 属性在生产配置文件中设置为 validate!这是咱们抵挡 Flyway 脚本谬误的最初一道防线!无论如何,让咱们确保将 ddl-auto 属性在生产配置文件中设置为 validate!这是咱们抵挡 Flyway 脚本谬误的最初一道防线!

应用 Liquibase

Liquibase 是另一种数据库迁徙工具,其工作形式相似于 Flyway,但反对除 SQL 之外的其余输出格局。例如,咱们能够提供定义数据库架构的 YAML 或 XML 文件。
咱们只需增加依赖项即可激活它:

compile('org.liquibase:liquibase-core')

默认状况下,Liquibase 将主动创立在 src/main/resources/db/changelog/db.changelog-master.yaml 中定义的模式。
同样,设置 ddl-autovalidate 是有意义的:

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {"spring.jpa.hibernate.ddl-auto=validate"})
class LiquibaseTest {...}

在测试中应用 Liquibase 的价值

因为 Liquibase 容许多种输出格局充当 SQL 上的形象层,因而即便它们的 SQL 方言不同,也能够跨多个数据库应用雷同的脚本。这使得在咱们的测试和生产中应用雷同的 Liquibase 脚本成为可能。
不过,YAML 格局十分敏感,而且我最近在保护大型 YAML 文件汇合时遇到了麻烦。这一点,以及只管咱们实际上必须为不同的数据库编辑这些文件的形象,最终导致转向 Flyway。

填充数据库

当初咱们曾经为咱们的测试创立了一个数据库模式,咱们终于能够开始理论的测试了。在数据库查问测试中,咱们通常会向数据库增加一些数据,而后验证咱们的查问是否返回正确的后果。
同样,有多种办法能够将数据增加到咱们的内存数据库中,所以让咱们逐个探讨。

应用 data.sql

schema.sql 相似,咱们能够应用蕴含插入语句的 data.sql 文件来填充咱们的数据库。上述规定同样实用。

可维护性

data.sql 文件迫使咱们将所有 insert 语句放在一个中央。每一个测试都将依赖于这个脚本来设置数据库状态。这个脚本很快就会变得十分大并且难以保护。如果有须要抵触数据库状态的测试怎么办?
因而,应审慎思考这种办法。

手动插入实体

为每个测试创立特定数据库状态的最简略办法是在运行被测查问之前在测试中保留一些实体:

@Test
void whenSaved_thenFindsByName() {
  userRepository.save(new UserEntity(
          "Zaphod Beeblebrox",
          "zaphod@galaxy.net"));
  assertThat(userRepository.findByName("Zaphod Beeblebrox")).isNotNull();}

这对于下面示例中的简略实体来说很容易。但在理论我的项目中,这些实体的构建和与其余实体的关系通常要简单得多。此外,如果咱们想测试比 findByName 更简单的查问,很可能咱们须要创立比单个实体更多的数据。这很快变得十分令人腻烦。
管制这种复杂性的一种办法是创立工厂办法,可能联合 Objectmother 和 Builder 模式。
在 Java 代码中“手动”对数据库进行编程的办法比其余办法有很大的劣势,因为 它是重构平安的。代码库中的更改会导致咱们的测试代码中呈现编译谬误。在所有其余办法中,咱们必须运行测试能力收到无关重构导致的潜在谬误的告诉。应用

Spring DBUnit

DBUnit 是一个反对将数据库设置为某种状态的库。Spring DBUnit 将 DBUnit 与 Spring 集成在一起,因而它能够主动与 Spring 的事务等一起工作。
要应用它,咱们须要向 Spring DBUnit 和 DBUnit 增加依赖项:

compile('com.github.springtestdbunit:spring-test-dbunit:1.3.0')
compile('org.dbunit:dbunit:2.6.0')

而后,对于每个测试,咱们能够创立一个蕴含所需数据库状态的自定义 XML 文件:

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <user
        id="1"
        name="Zaphod Beeblebrox"
        email="zaphod@galaxy.net"
    />
</dataset>

默认状况下,XML 文件(咱们将其命名为 createUser.xml)位于测试类旁边的类门路中。
在测试类中,咱们须要增加两个 TestExecutionListeners 来启用 DBUnit 反对。要设置某个数据库状态,咱们能够在测试方法上应用 @DatabaseSetup

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestExecutionListeners({
        DependencyInjectionTestExecutionListener.class,
        TransactionDbUnitTestExecutionListener.class
})
class SpringDbUnitTest {

  @Autowired
  private UserRepository userRepository;

  @Test
  @DatabaseSetup("createUser.xml")
  void whenInitializedByDbUnit_thenFindsByName() {UserEntity user = userRepository.findByName("Zaphod Beeblebrox");
    assertThat(user).isNotNull();}
}

对于更改数据库状态的测试查问,咱们甚至能够应用 @ExpectedDatabase 来定义数据库在测试 预期处于的状态。
然而请留神,自 2016 年以来,Spring DBUnit 没有再保护。

@DatabaseSetup 不起作用?

在我的测试中,我遇到了 @DatabaseSetup 正文被默默疏忽的问题。原来有一个 ClassNotFoundException 因为找不到某些 DBUnit 类。不过,这个异样被吞了。
起因是我遗记蕴含对 DBUnit 的依赖,因为我认为 Spring Test DBUnit 可递进地含它。因而,如果您遇到雷同的问题,请查看您是否蕴含了这两个依赖项。

应用 @Sql

一个十分类似的办法是应用 Spring 的 @Sql 注解。咱们没有应用 XML 来形容数据库状态,而是间接应用 SQL:

INSERT INTO USER
            (id,
             NAME,
             email)
VALUES      (1,
             'Zaphod Beeblebrox',
             'zaphod@galaxy.net');

在咱们的测试中,咱们能够简略地应用 @Sql 注解来援用 SQL 文件来填充数据库:

@ExtendWith(SpringExtension.class)
@DataJpaTest
class SqlTest {

  @Autowired
  private UserRepository userRepository;

  @Test
  @Sql("createUser.sql")
  void whenInitializedByDbUnit_thenFindsByName() {UserEntity user = userRepository.findByName("Zaphod Beeblebrox");
    assertThat(user).isNotNull();}

}

如果咱们须要多个脚本,咱们能够应用 @SqlGroup 来组合它们。

论断

为了测试数据库查问,咱们须要创立模式并用一些数据填充它的办法。因为测试应该互相独立,因而最好对每个测试别离执行此操作。
对于简略的测试和简略的数据库实体,通过创立和保留 JPA 实体手动创立状态就足够了。对于更简单的场景,@DatabaseSetup@Sql 提供了一种在 XML 或 SQL 文件中内部化数据库状态的办法。

正文完
 0