【注】本文翻译自:TestingJPAQuerieswithSpringBootand@DataJpaTest-Reflectoring除了单元测试,集成测试在生产高质量软件中起着至关重要的作用.一种特殊的集成测试处理我们的代码和数据库之间的集成。通过@DataJpaTest注解,SpringBoot提供了一种便捷的方式来设置带有嵌入式数据库的环境来测试我们的数据库查询。在本教程中,我们将首先讨论哪些类型的查询值得测试,然后讨论创建数据库模式和数据库状态以进行测试的不同方法。?代码示例本文在GitHub依赖项上提供了一个工作代码示例在本教程中,除了通常的SpringBoot依赖项之外,我们还使用JUnitJupiter作为我们的测试框架,并使用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对象的SpringData存储库:interfaceUserRepositoryextendsCrudRepository{//querymethods}我们有不同的选项来创建查询。让我们详细看看其中的一些,以确定我们是否应该用测试来覆盖它们。推断查询第一个选项是创建一个推断查询:UserEntityfindByName(Stringname);我们不需要告诉SpringData做什么,因为它会自动从方法名称的名称中推断出SQL查询。此功能的好处是SpringData还会在启动时自动检查查询是否有效。如果我们将方法重命名为findByFoo()并且UserEntity没有属性foo,SpringData会抛出一个异常来指出这一点:org.springframework.data.mapping.PropertyReferenceException:NopropertyfoofoundfortypeUserEntity!因此,只要我们至少有一个尝试在我们的代码库中启动Spring应用程序上下文的测试,我们就不需要为我们的推理查询编写额外的测试。请注意,对于从长方法名称(如findByNameAndRegistrationDateBeforeAndEmailIsNotNull())推断出的查询,情况并非如此。这个方法名称很棘手且容易出错,因此我们应该测试它是否确实按照我们的预期进行。话虽如此,将此类方法重命名为更短、更有意义的名称并添加@Query注释以提供自定义JPQL查询是一种很好的做法。使用@Query自定义JPQL查询如果查询变得更复杂,提供自定义JPQL查询是有意义的:@Query("selectufromUserEntityuwhereu.name=:name")UserEntityfindByNameCustomQuery(@Param("name")字符串名称);与推理查询类似,我们可以免费对这些JPQL查询执行有效性检查。使用Hibernate作为我们的JPA提供者,如果发现无效查询,我们将在启动时得到一个QuerySyntaxException:org.hibernate.hql.internal.ast.QuerySyntaxException:unexpectedtoken:foonearline1,column64[selectufrom...]然而,自定义查询比通过单个属性查找条目要复杂得多。例如,它们可能包括与其他表的连接或返回复杂的DTO而不是实体。那么,我们应该为自定义查询编写测试吗?令人不满意的答案是我们必须自己决定查询是否复杂到足以进行测试。使用@Query的本机查询另一种方法是使用本机查询:@Query(value="select*fromuserasuwhereu.name=:name",nativeQuery=true)UserEntityfindByNameNativeQuery(@Param("name")Stringname);我们不指定JPQL查询(它是对SQL的抽象),而是直接指定SQL查询。此查询可能使用特定数据库的SQL方言。重要的是要注意Hibernate和SpringData都不会在启动时验证本机查询。由于查询可能包含特定于数据库的SQL,因此SpringData或Hibernate无法知道要检查的内容。因此,本机查询是集成测试的主要候选对象。但是,如果他们真的使用特定于数据库的SQL,那么这些测试可能不适用于嵌入式内存数据库,因此我们必须在后台提供一个真实的数据库(比如,在持续集成中按需设置的docker容器中)管道)。@DataJpaTest简介为了测试SpringDataJPA存储库或任何其他与JPA相关的组件,SpringBoot提供了@DataJpaTest注解。我们可以将它添加到我们的单元测试中,这将设置一个Spring应用程序上下文:@ExtendWith(SpringExtension.class)@DataJpaTestclassUserEntityRepositoryTest@Autowired私有JdbcTemplatejdbcTemplate;@AutowiredprivateEntityManager实体管理器;@AutowiredprivateUserRepositoryuserRepository;@TestvoidinjectedComponentsAreNotNull(){assertThat(dataSource).isNotNull();assertThat(jdbcTemplate).isNotNull();assertThat(entityManager).isNotNull();assertThat(userRepository).isNotNull();}}ExtendWith本教程中的代码示例使用@ExtendWith注释来告诉JUnit5启用Spring支持。从SpringBoot2.1开始,我们不再需要加载SpringExtension,因为它作为元注释包含在SpringBoot测试注释中,例如@DataJpaTest、@WebMvcTest和@SpringBootTest。本教程中的代码示例使用@ExtendWith批注告诉JUnit5启用Spring支持。从SpringBoot2.1开始,我们不再需要加载SpringExtension,因为它作为元注释包含在SpringBoot测试注释中,例如@DataJpaTest、@WebMvcTest和@SpringBootTest。这样创建的应用程序上下文不会包含我们的SpringBoot应用程序所需的整个上下文,而只是其中的一个“片段”,其中包含初始化任何JPA相关组件(例如我们的SpringData存储库)所需的组件。例如,如果需要,我们可以将DataSource、@JdbcTemplate或@EntityManage注入到我们的测试类中。此外,我们可以从我们的应用程序中注入任何SpringData存储库。以上所有组件都将自动配置为指向嵌入式内存数据库,而不是我们可能在application.properties或application.yml文件中配置的“真实”数据库。请注意,默认情况下,包含所有这些组件(包括内存数据库)的应用程序上下文在所有@DataJpaTest注释测试类中的所有测试方法之间共享。这就是为什么默认情况下每个测试方法都在其自己的事务中运行,该事务在方法执行后回滚。这样,数据库状态在测试之间保持原始状态,并且测试保持彼此独立。创建数据库模式在我们可以测试对数据库的任何查询之前,我们需要创建一个SQL模式以供使用。让我们看看一些不同的方法来做到这一点。使用Hibernateddl-auto默认情况下,@DataJpaTest将Hibernate配置为自动为我们创建数据库模式。负责此的属性是spring.jpa.hibernate.ddl-auto,SpringBoot默认将其设置为create-drop,这意味着模式在运行测试之前创建并在测试执行之后删除。因此,如果我们对Hibernate为我们创建模式感到满意,我们就不必做任何事情。使用schema.sqlSpringBoot支持在应用程序启动时执行自定义schema.sql文件。如果Spring在类路径上找到schema.sql文件,它将针对数据源执行。这会覆盖上面讨论的Hibernateddl-auto配置。我们可以使用属性spring.datasource.initialization-mode来控制是否应该执行schema.sql。默认是嵌入式的,这意味着它只会针对嵌入式数据库执行(即在我们的测试中)。如果我们设置为always,它就会一直执行。以下日志输出确认文件已执行:ExecutingSQLscriptfromURL[file:.../out/production/resources/schema.sql]设置Hibernate的ddl-auto配置以在使用脚本时进行验证是有意义的初始化架构,以便Hibernate在启动时检查创建的架构是否与实体类匹配:@ExtendWith(SpringExtension.class)@DataJpaTest@TestPropertySource(properties={"spring.jpa.hibernate.ddl-auto=validate"})classSchemaSqlTest{...}使用FlywayFlyway是一个数据库迁移工具,它允许指定多个SQL脚本来创建数据库模式。它会跟踪这些脚本中哪些已经在目标数据库上执行过,以便只执行之前未执行过的脚本。要激活Flyway,我们只需要将依赖项放入我们的build.gradle文件中(如果我们使用Maven,则类似):compile('org.flywaydb:flyway-core')如果我们没有专门为Hibernate-auto配置的ddl配置,它会自动退出,因此Flyway具有优先权,默认情况下会针对我们的内存数据库测试执行它在文件夹src/main/resources/db/migration中找到的所有SQL脚本。此外,将ddl-auto设置为验证是有意义的,让Hibernate检查Flyway生成的模式是否与我们的Hibernate实体所期望的相匹配:@ExtendWith(SpringExtension.class)@DataJpaTest@TestPropertySource(properties={"spring.jpa.hibernate.ddl-auto=validate"})classFlywayTest{...}在测试中使用Flyway的价值如果我们在生产中使用Flyway并如上所述在JPA测试中使用它会很好。只有这样我们才能知道flyway脚本在测试的时候是按预期工作的。但是,这仅在脚本包含对生产数据库和测试中使用的内存数据库(我们示例中的H2数据库)均有效的SQL时有效。如果不是这种情况,我们必须在测试中禁用Flyway,方法是将spring.flyway.enabled属性设置为false并将spring.jpa.hibernate.ddl-auto属性设置为create-drop,让Hibernate生成模型。无论如何,让我们确保在生产配置文件中将ddl-auto属性设置为验证!这是我们防止Flyway脚本错误的最后一道防线!无论如何,让我们确保在生产配置文件中将ddl-auto属性设置为验证!这是我们防止Flyway脚本错误的最后一道防线!使用LiquibaseLiquibase是另一个数据库迁移工具,其工作方式类似于Flyway,但支持SQL以外的其他输入格式。例如,我们可以提供定义数据库架构的YAML或XML文件。我们可以简单地通过添加依赖来激活它:compile('org.liquibase:liquibase-core')默认情况下,Liquibase会自动创建在src/main/resources/db/changelog/db.changelog-master.yaml中定义模式。此外,将ddl-auto设置为验证是有意义的:@ExtendWith(SpringExtension.class)@DataJpaTest@TestPropertySource(properties={"spring.jpa.hibernate.ddl-auto=validate"})classLiquibaseTest{...}在测试中使用Liquibase的价值由于Liquibase允许多种输入格式充当SQL之上的抽象层,因此您可以在多个数据库中使用相同的脚本,即使它们具有不同的SQL方言。这使得在我们的测试和生产中使用相同的Liquibase脚本成为可能。然而,YAML格式非常敏感,我最近在维护大量YAML文件时遇到了麻烦。这一点,以及尽管我们实际上必须为不同的数据库编辑这些文件的抽象,最终导致了迁移到Flyway。填充数据库现在我们已经为我们的测试创建了一个数据库模式,我们终于可以开始实际的测试了。在数据库查询测试中,我们通常会向数据库中添加一些数据,然后验证我们的查询是否返回正确的结果。同样,有多种方法可以将数据添加到我们的内存数据库中,所以让我们一一讨论。使用data.sql与schema.sql类似,我们可以使用包含插入语句的data.sql文件填充我们的数据库。适用与上述相同的规则。可维护性data.sql文件迫使我们将所有插入语句放在一个地方。每个测试都将依赖此脚本来设置数据库状态。这个脚本很快就会变得非常大并且难以维护。如果有测试需要冲突的数据库状态怎么办?因此,应该仔细考虑这种方法。手动插入实体为每个测试创建特定数据库状态的最简单方法是在运行被测查询之前在测试中保存一些实体:@TestvoidwhenSaved_thenFindsByName(){userRepository.save(newUserEntity("ZaphodBeeblebrox","zaphod@galaxy.net"));assertThat(userRepository.findByName("ZaphodBeeblebrox")).isNotNull();}这对于上面示例中的简单实体来说已经足够简单了。但在实际项目中,这些实体的构建以及与其他实体的关系通常要复杂得多。此外,如果我们想测试比findByName更复杂的查询,很可能我们需要创建比单个实体更多的数据。这很快就会变得非常令人厌烦。控制这种复杂性的一种方法是创建工厂方法,可能会结合Objectmother和Builder模式。用Java代码“手动”编程数据库的方法比其他方法有很大的优势,因为它是重构安全的。代码库的更改会导致我们的测试代码出现编译错误。在所有其他方法中,我们必须运行测试以通知重构导致的潜在错误。使用SpringDBUnitDBUnit是一个支持将数据库设置为特定状态的库。SpringDBUnit将DBUnit与Spring集成,因此它可以自动处理Spring的事务等。要使用它,我们需要向SpringDBUnit和DBUnit添加依赖项:compile('com.github.springtestdbunit:spring-test-dbunit:1.3.0')compile('org.dbunit:dbunit:2.6.0')然后,对于每个测试,我们可以创建一个包含所需数据库状态的自定义XML文件:默认情况下,XML文件(我们将其命名为createUser.xml)位于测试类旁边的类路径中。在测试类中,我们需要添加两个TestExecutionListener以启用DBUnit支持。要设置特定的数据库状态,我们可以在测试方法上使用@DatabaseSetup:@Test@DatabaseSetup("createUser.xml")voidwhenInitializedByDbUnit_thenFindsByName(){UserEntityuser=userRepository.findByName("ZaphodBeeblebrox");assertThat(用户).isNotNull();}}对于改变数据库状态的测试查询,我们甚至可以使用@ExpectedDatabase来定义数据库在测试后的预期状态。但是请注意,自2016年以来,SpringDBUnit一直没有得到维护。@DatabaseSetup不起作用?在我的测试中,我遇到了@DatabaseSetup注释被默默忽略的问题。原来有一个ClassNotFoundException因为找不到一些DBUnit类。但是,异常被吞噬了。原因是我忘记包含对DBUnit的依赖,因为我认为SpringTestDBUnit递归包含它。因此,如果您遇到同样的问题,请检查您是否包含了这两个依赖项。使用@Sql的一种非常相似的方法是使用Spring的@Sql注释。我们没有使用XML来描述数据库状态,而是直接使用SQL:INSERTINTOUSER(id,NAME,email)VALUES(1,'ZaphodBeeblebrox','zaphod@galaxy.net');在我们的测试中,我们可以简单地使用@Sql注解来引用SQL文件来填充数据库:@ExtendWith(SpringExtension.class)@DataJpaTestclassSqlTest{@AutowiredprivateUserRepositoryuserRepository;@Test@Sql("createUser.sql")voidwhenInitializedByDbUnit_thenFindsByName(){UserEntityuser=userRepository.findByName("ZaphodBeeblebrox");assertThat(用户).isNotNull();如果我们需要多个脚本,我们可以使用@SqlGroup将它们分组。结论为了测试数据库查询,我们需要一种方法来创建模式并用一些数据填充它。由于测试应该相互独立,因此最好分别为每个测试执行此操作。对于简单的测试和简单的数据库实体,通过创建和保存JPA实体手动创建状态就足够了。对于更复杂的场景,@DatabaseSetup和@Sql提供了一种在XML或SQL文件中外部化数据库状态的方法。