在Spring Boot测试中使用Testcontainer进行数据库集成测试

在此博客文章中,我想演示如何在Spring Boot测试中集成Testcontainer以便与数据库一起运行集成测试。 我没有使用Testcontainers的Spring Boot模块。 如何与他们合作,我将在另一篇博客文章中进行介绍。 所有示例都可以在GitHub上找到 。

为什么要使用测试容器?

Testcontainers是一个库,可帮助在基于Docker容器的集成测试中集成数据库等基础架构组件。 它有助于避免编写集成测试。 这些是根据另一个系统的正确性通过或失败的测试。 使用Testcontainer,我可以控制这些从属系统。

域介绍

进一步的示例展示了不同的方法,该方法如何通过数据库中不同的存储库实现来保存一些英雄对象,以及相应的测试如何。

package com.github.sparsick.testcontainerspringboot.hero.universum;import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;public class Hero {private Long id;private String name;private String city;private ComicUniversum universum;public Hero(String name, String city, ComicUniversum universum) {this.name = name;this.city = city;this.universum = universum;}public String getName() {return name;}public String getCity() {return city;}public ComicUniversum getUniversum() {return universum;}
}

所有其他存储库都是Spring Boot Web应用程序的一部分。 因此,在本博客文章的结尾,我将演示如何为整个Web应用程序(包括数据库)编写测试。 让我们从一个简单的示例开始,该示例基于JDBC。

基于JDBC测试存储库

假设我们有以下基于JDBC的存储库实现。 我们有两种方法,一种是将英雄添加到数据库中,另一种是从数据库中获取所有英雄。

package com.github.sparsick.testcontainerspringboot.hero.universum;import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;import javax.sql.DataSource;
import java.util.Collection;@Repository
public class HeroClassicJDBCRepository {private final JdbcTemplate jdbcTemplate;public HeroClassicJDBCRepository(DataSource dataSource) {jdbcTemplate = new JdbcTemplate(dataSource);}public void addHero(Hero hero) {jdbcTemplate.update("insert into hero (city, name, universum) values (?,?,?)",hero.getCity(), hero.getName(), hero.getUniversum().name());}public CollectionallHeros() {return jdbcTemplate.query("select * From hero",(resultSet, i) -> new Hero(resultSet.getString("name"),resultSet.getString("city"),ComicUniversum.valueOf(resultSet.getString("universum"))));}}

对于此存储库,我们可以编写普通的JUnit5测试,而无需加载Spring应用程序上下文。 因此,首先,我们必须建立对测试库的依赖关系,在这种情况下为JUnit5和Testcontainers。 作为构建工具,我使用Maven。 这两个测试库都提供了所谓的BOM“物料清单” ,这有助于避免我所使用的依赖项中的版本不匹配。 作为数据库,我想使用MySQL。 因此,除了核心模块testcontainers之外,我还使用了Testcontainers的模块mysql 。 它提供了一个预定义的MySQL容器。 为了简化JUnit5测试代码中专门的容器设置,Testcontainers提供了一个JUnit5模块junit-jupiter

<dependencies><dependency><groupId>org.testcontainers</groupId><artifactId>testcontainers</artifactId><scope>test</scope></dependency><dependency><groupId>org.testcontainers</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency><dependency><groupId>org.testcontainers</groupId><artifactId>mysql</artifactId><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency>
</dependencies>
<dependencyManagement><dependencies><dependency><groupId>org.junit</groupId><artifactId>junit-bom</artifactId><version>${junit.jupiter.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.testcontainers</groupId><artifactId>testcontainers-bom</artifactId><version>${testcontainers.version}</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

现在,我们拥有编写第一个测试的所有内容。

package com.github.sparsick.testcontainerspringboot.hero.universum;import ...@Testcontainers
class HeroClassicJDBCRepositoryIT {@Containerprivate MySQLContainer database = new MySQLContainer();private HeroClassicJDBCRepository repositoryUnderTest;@Testvoid testInteractionWithDatabase() {ScriptUtils.runInitScript(new JdbcDatabaseDelegate(database, ""),"ddl.sql");repositoryUnderTest = new HeroClassicJDBCRepository(dataSource());repositoryUnderTest.addHero(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));Collection<Hero> heroes = repositoryUnderTest.allHeros();assertThat(heroes).hasSize(1);}@NotNullprivate DataSource dataSource() {MysqlDataSource dataSource = new MysqlDataSource();dataSource.setUrl(database.getJdbcUrl());dataSource.setUser(database.getUsername());dataSource.setPassword(database.getPassword());return dataSource;}
}

让我们看看如何为测试准备数据库。 首先,我们使用@Testcontainers注释测试类。 该注释的后面隐藏了Testcontainers提供的JUnit5扩展。 它检查Docker是否安装在计算机上,并在测试期间启动和停止容器。 但是,Testcontainers如何知道应该从哪个容器开始? 在这里,注释@Container帮助。 它标记了应由Testcontainers扩展管理的容器。 在这种情况下,一个MySQLContainer由Testcontainers模块提供mysql 。 此类提供了MySQL Docker容器,并处理诸如设置数据库用户,识别何时可以使用数据库等问题。一旦数据库准备就绪可以使用,就必须设置数据库架构。 测试容器也可以在此处提供支持。 ScriptUtils. runInitScript (new JdbcDatabaseDelegate(database, ""),"ddl.sql"); 确保按照SQL脚本ddl.sql定义的那样设置架构。

-- ddl.sql
create table hero (id bigint AUTO_INCREMENT PRIMARY KEY, city varchar(255), name varchar(255), universum varchar(255)) engine=InnoDB

现在,我们准备建立受测试的存储库。 因此,我们需要DataSource对象的数据库连接信息。 在底层,Testcontainers会搜索可用的端口,并将容器绑定到该空闲端口上。 在每个通过Testcontainer启动的容器上,此端口号均不同。 此外,它使用用户名和密码在容器中配置数据库。 因此,我们必须询问MySQLContainer对象数据库凭据和JDBC URL的状态。 有了这些信息,我们就可以建立被测试的存储库( repositoryUnderTest = new HeroClassicJDBCRepository(dataSource()); )并完成测试。

如果运行测试,则会收到以下错误消息:

17:18:50.990 [ducttape-1] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@1adc57a8
17:18:51.492 [ducttape-1] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - Pinging docker daemon...
17:18:51.493 [ducttape-1] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@3e5b3a3b
17:18:51.838 [main] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - UnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause LastErrorException ([111] Verbindungsaufbau abgelehnt)
17:18:51.851 [main] DEBUG org.rnorth.tcpunixsocketproxy.ProxyPump - Listening on localhost/127.0.0.1:41039 and proxying to /var/run/docker.sock
17:18:51.996 [ducttape-0] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - Pinging docker daemon...
17:18:51.997 [ducttape-1] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - Pinging docker daemon...
17:18:51.997 [ducttape-0] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@5d43d23e
17:18:51.997 [ducttape-1] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory$1@7abf08d2
17:18:52.002 [tcp-unix-proxy-accept-thread] DEBUG org.rnorth.tcpunixsocketproxy.ProxyPump - Accepting incoming connection from /127.0.0.1:41998
17:19:01.866 [main] DEBUG org.testcontainers.dockerclient.DockerClientProviderStrategy - ProxiedUnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause TimeoutException (null)
17:19:01.870 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
17:19:01.872 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed)
17:19:01.873 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed)
17:19:01.874 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     UnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause LastErrorException ([111] Verbindungsaufbau abgelehnt)
17:19:01.875 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy -     ProxiedUnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause TimeoutException (null)
17:19:01.875 [main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - As no valid configuration was found, execution cannot continue
17:19:01.900 [main] DEBUG🐳 [mysql:5.7.22] - mysql:5.7.22 is not in image name cache, updating...
Mai 01, 2020 5:19:01 NACHM. org.junit.jupiter.engine.execution.JupiterEngineExecutionContext close
SEVERE: Caught exception while closing extension context: org.junit.jupiter.engine.descriptor.MethodExtensionContext@2e6a5539
org.testcontainers.containers.ContainerLaunchException: Container startup failedat org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:322)at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:302)at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.start(TestcontainersExtension.java:173)at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.access$100(TestcontainersExtension.java:160)at org.testcontainers.junit.jupiter.TestcontainersExtension.lambda$null$3(TestcontainersExtension.java:50)at org.junit.jupiter.engine.execution.ExtensionValuesStore.lambda$getOrComputeIfAbsent$0(ExtensionValuesStore.java:81)at org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.get(ExtensionValuesStore.java:182)at org.junit.jupiter.engine.execution.ExtensionValuesStore.closeAllStoredCloseableValues(ExtensionValuesStore.java:58)at org.junit.jupiter.engine.descriptor.AbstractExtensionContext.close(AbstractExtensionContext.java:73)at org.junit.jupiter.engine.execution.JupiterEngineExecutionContext.close(JupiterEngineExecutionContext.java:53)at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.cleanUp(JupiterTestDescriptor.java:222)at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.cleanUp(JupiterTestDescriptor.java:57)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$cleanUp$9(NodeTestTask.java:151)at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)at org.junit.platform.engine.support.hierarchical.NodeTestTask.cleanUp(NodeTestTask.java:151)at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:83)at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: org.testcontainers.containers.ContainerFetchException: Can't get Docker image: RemoteDockerImage(imageNameFuture=java.util.concurrent.CompletableFuture@539d019[Completed normally], imagePullPolicy=DefaultPullPolicy(), dockerClient=LazyDockerClient.INSTANCE)at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1265)at org.testcontainers.containers.GenericContainer.logger(GenericContainer.java:600)at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:311)... 47 more
Caused by: java.lang.IllegalStateException: Previous attempts to find a Docker environment failed. Will not retry. Please see logs and check configurationat org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:78)at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:115)at org.testcontainers.LazyDockerClient.getDockerClient(LazyDockerClient.java:14)at org.testcontainers.LazyDockerClient.inspectImageCmd(LazyDockerClient.java:12)at org.testcontainers.images.LocalImagesCache.refreshCache(LocalImagesCache.java:42)at org.testcontainers.images.AbstractImagePullPolicy.shouldPull(AbstractImagePullPolicy.java:24)at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:62)at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:25)at org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20)at org.testcontainers.utility.LazyFuture.get(LazyFuture.java:27)at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1263)... 49 moreorg.testcontainers.containers.ContainerLaunchException: Container startup failed

该错误消息表示Docker守护程序未运行。 确保Docker守护程序正在运行后,测试运行将成功。

控制台输出中有很多调试消息。 测试中的日志记录输出可以通过src/test/resourceslogback.xml文件进行配置:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration><include resource="org/springframework/boot/logging/logback/base.xml"/><root level="info"><appender-ref ref="CONSOLE" /></root>
</configuration>

有关日志记录的Spring Boot文档建议使用logback-spring.xml作为配置文件。 但是普通的JUnit5测试无法识别它,只有@SpringBootTest注释了测试。 两种测试都使用logback.xml

基于JPA实体管理器测试存储库

现在,我们要使用经典的实体管理器来实现基于JPA的存储库。 假设,我们通过三种方法执行以下操作:将英雄添加到数据库中,通过搜索条件查找英雄,并从数据库中获取所有英雄。 实体管理器由Spring的应用程序上下文配置( @PersistenceContext负责)。

package com.github.sparsick.testcontainerspringboot.hero.universum;import ...@Repository
public class HeroClassicJpaRepository {@PersistenceContextprivate EntityManager em;@Transactionalpublic void addHero(Hero hero) {em.persist(hero);}public CollectionallHeros() {return em.createQuery("Select hero FROM Hero hero", Hero.class).getResultList();}public CollectionfindHerosBySearchCriteria(String searchCriteria) {return em.createQuery("SELECT hero FROM Hero hero " +"where hero.city LIKE :searchCriteria OR " +"hero.name LIKE :searchCriteria OR " +"hero.universum = :searchCriteria",Hero.class).setParameter("searchCriteria", searchCriteria).getResultList();}}

作为JPA的实现,我们选择Hibernate和MySQL作为数据库提供程序。 我们必须配置休眠应使用的方言。

# src/main/resources/application.properties
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

application.properties您还配置数据库连接等。

为了在测试中正确设置实体管理器,我们必须在应用程序上下文中运行测试,以便Spring可以正确配置实体管理器。

Spring Boot带来了一些测试支持类。 因此,我们必须向该项目添加进一步的测试依赖项。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>

该入门程序还包括JUnit Jupiter依赖关系和其他测试库的依赖关系,因此您可以根据需要从依赖关系声明中删除这些依赖关系。

现在,我们拥有编写测试的所有内容。

package com.github.sparsick.testcontainerspringboot.hero.universum;import ...@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = HeroClassicJpaRepositoryTest.Initializer.class)
class HeroClassicJpaRepositoryIT {@Containerprivate static MySQLContainer database = new MySQLContainer();@Autowiredprivate HeroClassicJpaRepository repositoryUnderTest;@Testvoid findHeroByCriteria(){repositoryUnderTest.addHero(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));Collectionheros = repositoryUnderTest.findHerosBySearchCriteria("Batman");assertThat(heros).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));}static class Initializer implementsApplicationContextInitializer{public void initialize(ConfigurableApplicationContextconfigurableApplicationContext) {TestPropertyValues.of("spring.datasource.url=" + database.getJdbcUrl(),"spring.datasource.username=" + database.getUsername(),"spring.datasource.password=" + database.getPassword()).applyTo(configurableApplicationContext.getEnvironment());}}
}

测试类带有一些注释。 第一个是SpringBootTest从而在测试期间启动Spring应用程序上下文。 下一个是@Testcontainers 。 从上次测试中我们已经知道了该注释。 它是一个JUnit5扩展,用于管理测试期间启动和停止docker容器。 最后一个是@ContextConfiguration(initializers = HeroClassicJpaRepositoryTest.Initializer.class)因此我们可以以编程方式配置应用程序上下文。 在我们的例子中,我们想用从Testcontainers管理的数据库容器对象获得的数据库信息覆盖数据库连接配置。 就像我们在上面的JDBC测试中看到的那样,我们注释数据库容器private static MySQLContainer database = new MySQLContainer();@Container 。 它表明此容器应由Testcontainers管理。 这与上面的JDBC设置略有不同。 在这里, MySQLContainer databasestatic ,在JDBC设置中它是一个普通的类字段。 这里,它必须是静态的,因为容器必须在应用程序上下文启动之前启动,以便我们进行更改以将数据库连接配置传递给应用程序上下文。 为此, static class Initializer负责。 在启动阶段,它将覆盖应用程序上下文配置。 最后一步是在数据库中设置数据库架构。 JPA在这里可以提供帮助。 它可以自动创建数据库架构。 您必须使用

# src/test/resources/application.properties
spring.jpa.hibernate.ddl-auto=update

或者,您可以在static class Initializer添加此属性。

现在,我们可以将存储库注入测试( @Autowired private HeroClassicJpaRepository repositoryUnderTest )。 该存储库由Spring配置并可以进行测试。

基于Spring Data JPA测试存储库

如今,在Spring Boot应用程序中通常将JPA与Spring Data结合使用,因此我们重写存储库以使用Spring Data JPA代替纯JPA。 结果是扩展了Spring Data的CrudRepository的接口,因此我们具有所有基本操作,如保存,删除,通过id更新查找等。 对于按条件搜索功能,我们必须使用@Query注释定义一个具有JPA查询的方法。

package com.github.sparsick.testcontainerspringboot.hero.universum;import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;import java.util.List;public interface HeroSpringDataJpaRepository extends CrudRepository<Hero, Long> {@Query("SELECT hero FROM Hero hero where hero.city LIKE :searchCriteria OR hero.name LIKE :searchCriteria OR hero.universum = :searchCriteria")List<Hero> findHerosBySearchCriteria(@Param("searchCriteria") String searchCriteria);
}

正如上面在经典JPA示例中所提到的,在这里也是如此,我们必须配置Hibernate选择的JPA实现应使用哪种SQL方言,以及如何设置数据库模式。

与测试配置相同,同样,我们需要一个带有Spring应用程序上下文的测试,以便为测试正确配置存储库。 但是这里我们不需要使用@SpringBootTest来启动整个应用程序上下文。 相反,我们使用@DataJpaTest 。 该批注仅使用持久层所需的bean启动应用程序上下文。

package com.github.sparsick.testcontainerspringboot.hero.universum;import ...@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(initializers = HeroSpringDataJpaRepositoryIT.Initializer.class)
@Testcontainers
class HeroSpringDataJpaRepositoryIT {@Containerprivate static MySQLContainer database = new MySQLContainer();@Autowiredprivate HeroSpringDataJpaRepository repositoryUnderTest;@Testvoid findHerosBySearchCriteria() {repositoryUnderTest.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));Collection<Hero> heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");assertThat(heros).hasSize(1).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));}static class Initializer implementsApplicationContextInitializer<ConfigurableApplicationContext> {public void initialize(ConfigurableApplicationContextconfigurableApplicationContext) {TestPropertyValues.of("spring.datasource.url=" + database.getJdbcUrl(),"spring.datasource.username=" + database.getUsername(),"spring.datasource.password=" + database.getPassword()).applyTo(configurableApplicationContext.getEnvironment());}}
}

@DataJpaTest作为默认启动内存数据库。 但是我们希望使用由Testcontainers提供的容器化数据库。 因此,我们必须添加注释@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 。 这将禁用启动内存数据库。 其余测试配置与上述针对纯JPA示例的测试中的配置相同。

测试存储库但重用数据库

随着测试数量的增加,每次测试花费相当长的时间变得越来越重要,因为每次启动和初始化新数据库时,这种测试就变得越来越重要。 一种想法是在每次测试中重用数据库。 在这里, 单一容器模式可以提供帮助。 在所有测试开始运行之前,将启动并初始化一次数据库。 为此,每个需要数据库的测试都必须扩展一个抽象类,该类负责在所有测试运行之前启动和初始化数据库。

package com.github.sparsick.testcontainerspringboot.hero.universum;import ...@ContextConfiguration(initializers = DatabaseBaseTest.Initializer.class)
public abstract class DatabaseBaseTest {static final MySQLContainer DATABASE = new MySQLContainer();static {DATABASE.start();}static class Initializer implementsApplicationContextInitializer{public void initialize(ConfigurableApplicationContextconfigurableApplicationContext) {TestPropertyValues.of("spring.datasource.url=" + DATABASE.getJdbcUrl(),"spring.datasource.username=" + DATABASE.getUsername(),"spring.datasource.password=" + DATABASE.getPassword()).applyTo(configurableApplicationContext.getEnvironment());}}
}

在这个抽象类中,我们为扩展该抽象类和该数据库的应用程序上下文的所有测试配置一次启动的数据库。 请注意,这里我们不使用Testcontainers的注释,因为此注释会确保容器在每次测试后启动和停止。 但这我们会避免。 因此,我们自己启动数据库。 对于停止数据库,我们不需要注意。 为此,Testcontainers的侧车集装箱ryuk会非常小心。

现在,每个需要数据库的测试类都扩展了这个抽象类。 我们必须配置的唯一一件事就是应如何初始化应用程序上下文。 这意味着,当您需要整个应用程序上下文时,请使用@SpringBootTest 。 如果只需要持久层,则将@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class HeroSpringDataJpaRepositoryReuseDatabaseIT extends DatabaseBaseTest {@Autowiredprivate HeroSpringDataJpaRepository repositoryUnderTest;@Testvoid findHerosBySearchCriteria() {repositoryUnderTest.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));Collection<Hero> heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");assertThat(heros).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));}
}

测试包括数据库在内的整个Web应用程序

现在我们要测试整个应用程序,从控制器到数据库。 控制器实现如下所示:

@RestController
public class HeroRestController {private final HeroSpringDataJpaRepository heroRepository;public HeroRestController(HeroSpringDataJpaRepository heroRepository) {this.heroRepository = heroRepository;}@GetMapping("heros")public Iterable<Hero> allHeros(String searchCriteria) {if (searchCriteria == null || searchCriteria.equals("")) {return heroRepository.findAll();}return heroRepository.findHerosBySearchCriteria(searchCriteria);}@PostMapping("hero")public void hero(@RequestBody Hero hero) {heroRepository.save(hero);}
}

测试从数据库到控制器的整个过程的测试类看起来像这样

SpringBootTest
@ContextConfiguration(initializers = HeroRestControllerIT.Initializer.class)
@AutoConfigureMockMvc
@Testcontainers
class HeroRestControllerIT {@Containerprivate static MySQLContainer database = new MySQLContainer();@Autowiredprivate MockMvc mockMvc;@Autowiredprivate HeroSpringDataJpaRepository heroRepository;@Testvoid allHeros() throws Exception {heroRepository.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));heroRepository.save(new Hero("Superman", "Metropolis", ComicUniversum.DC_COMICS));mockMvc.perform(get("/heros")).andExpect(status().isOk()).andExpect(jsonPath("$[*].name", containsInAnyOrder("Batman", "Superman")));}static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext configurableApplicationContext) {TestPropertyValues.of("spring.datasource.url=" + database.getJdbcUrl(),"spring.datasource.username=" + database.getUsername(),"spring.datasource.password=" + database.getPassword()).applyTo(configurableApplicationContext.getEnvironment());}}
}

上一节中的测试为数据库和应用程序设置了测试。 一件事是不同的。 我们通过@AutoConfigureMockMvc添加了MockMVC支持。 这有助于通过HTTP层编写测试。

当然,您也可以使用扩展了抽象类DatabaseBaseTest的单个容器模式。

结论与概述

这篇博客文章展示了我们如何使用Testcontainers在Spring Boot中编写一些持久层实现的测试。 我们还将看到如何在多个测试中重用数据库实例,以及如何从控制器tor数据库为整个Web应用程序编写测试。 所有代码段都可以在GitHub上找到 。 在另一篇博客文章中,我将展示如何使用Testcontainers Spring Boot模块编写测试。

您还有其他针对持久层编写测试的想法吗? 请让我知道并写评论。

更多的信息

  1. BOM“物料清单”的概念
  2. 测试容器
  3. Spring Boot文档–日志记录
  4. Spring Boot文档–自动配置的数据JPA测试
  5. 测试容器–单容器模式
  6. Spring Boot文档– MockMVC
  7. GitHub存储库中的完整示例

翻译自: https://www.javacodegeeks.com/2020/05/using-testcontainers-in-spring-boot-tests-for-database-integration-tests.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/339782.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php模块安装 pdo_mysql_关于php插件pdo_mysql的安装

今天在做一个商城的连接的时候&#xff0c;需要MpDO验证。需要安装pdo_mysql模块&#xff0c;刚开始按照php扩展模块的安装按照这个安装ZIP,curl都成功了但是安装pdo_mysql却不行&#xff0c;在./configure --with-php-config/usr/local/php/bin/php-config是出现错误。如下che…

java8串行和并行的区别_垃圾收集器–串行,并行,CMS,G1(以及Java 8中的新增功能)...

java8串行和并行的区别4个Java垃圾收集器–错误的选择如何严重影响性能 在2014年&#xff0c;对于大多数开发人员来说&#xff0c;还有两件事仍然是个谜&#xff1a;垃圾收集和了解异性。 由于我对后者知之甚少&#xff0c;所以我认为我会对前者大吃一惊&#xff0c;尤其是因为…

python函数参数学习_python学习笔记-11.函数参数和返回值进阶

1. 函数参数和返回值的作用函数根据有没有参数以及有没有返回值&#xff0c;可以相互组合&#xff0c;共有4种形式&#xff1a;无参数&#xff0c;无返回值无参数&#xff0c;有返回值有参数&#xff0c;无返回值有参数&#xff0c;有返回值定义函数时&#xff0c;是否接收参数…

java spr_Java中的42行代码中的URL缩短器服务(Java(?!)Spring Boot + Redis

java spr显然&#xff0c;编写URL缩短服务是新的“ Hello&#xff0c;world&#xff01; ”在IoT /微服务/时代的世界中。 一切始于在45行Scala中的URL缩短服务 -整洁的Scala&#xff0c;以Spray和Redis进行调味以进行存储。 紧随其后的是&#xff0c; 在35行Clojure中提供了ur…

python邮件发送哪个好_python发邮件(一)

复习模块如果我们要发送邮件&#xff0c;就需要用到smtplib模块的以下方法&#xff1a;1、import smtplib #引入smtplib模块2、server smtplib.SMTP() #server为变量&#xff0c;通过SMTP指定一个服务器&#xff0c;这样才能把邮件送到另一个服务器&#xff0c;SMTP…

SSL Kafka经纪人从Kafka Mirror Maker迁移到Brooklin的挑战

问题 从卡夫卡镜子制造商转移到布鲁克林有我在这里所写的优势。 但是&#xff0c;进行这种迁移并不容易&#xff0c;因为它本来应该如此。 我面临的主要挑战是&#xff1a;在消费者Kafka经纪人和Brooklin之间建立SSL连接 解 SSL问题 事实证明&#xff0c;这个问题比我预期的要…

mysql移动数据的语句是_mysql基本语句

MySQL关系型数据库RDS中的老大哥&#xff0c;增删改查是MySQL入门的基础增删改查语句增删改查的语句命令为增&#xff1a;insert删&#xff1a;delete改&#xff1a;update查&#xff1a;SELECT或者show库操作创建数据库&#xff1a;create database shujukuba;创建带字符集的数…

jetty嵌入式容器_嵌入式Jetty和Apache CXF:借助Spring Security来保护REST服务

jetty嵌入式容器最近&#xff0c;我遇到了一个非常有趣的问题&#xff0c;我认为这只需要几分钟就可以解决&#xff1a;在Linux中使用Spring Security &#xff08;当前稳定版本3.2.5 &#xff09;保护Apache CXF &#xff08;当前版本3.0.1 &#xff09;/ JAX-RS REST服务。在…

python逻辑运算符的使用_总结Python中逻辑运算符的使用

下表列出了所有python语言支持的逻辑运算符。假设变量a持有10和变量b持有20&#xff0c;则&#xff1a;示例:试试下面的例子就明白了所有的python编程语言提供了逻辑运算符&#xff1a;#!/usr/bin/pythona 10b 20c 0if ( a and b ):print "line 1 - a and b are true&q…

如何使用用户数据脚本在EC2实例上安装Apache Web Server

你好朋友&#xff0c; 在本教程中&#xff0c;我们将看到如何使用用户数据脚本在EC2实例上安装Apache Web Server。 在我以前的教程之一中&#xff0c;我已经解释了如何使用AWS控制台启动EC2实例。如果您还没有完成该操作&#xff0c;我建议您先进行一下操作。 首先&#xf…

java 常量接口_java接口定义常量研究

背景和同事讨论到&#xff0c;在接口中定义常量的问题&#xff0c;引发了争论&#xff0c;即&#xff0c;假如在接口中定义变量&#xff0c;是否需要用static来限定的问题&#xff0c;或者说用static和不用static会有什么区别。引论package spring.interfaceTest;public interf…

spring 多租户_使用Spring Security的多租户应用程序的无状态会话

spring 多租户从前&#xff0c; 我发表了一篇文章&#xff0c;解释了构建无状态会话的原理 。 巧合的是&#xff0c;我们再次为多租户应用程序执行同一任务。 这次&#xff0c;我们将解决方案集成到Spring Security框架中&#xff0c;而不是自己构建身份验证机制。 本文将解释…

java api 1.6 下载_Java JDK API

JDKJavaDevelopmentKit是SunMicrosystems针对Java开发员的产品。自从Java推出以来&#xff0c;JDK已经成为使用最广泛的JavaSDK。JDK是整个Java的核心&#xff0c;包括了Java运行环境。相关软件软件大小版本说明下载地址jdk(Java Development Kit)是Sun Microsystems针对java开…

Java / Cloud:如何快速创建支持Kubernetes的REST微服务

可以肯定地说&#xff0c;如今微服务与云的结合风靡一时。 微服务的开发比以往任何时候都多&#xff0c;从而导致应用程序部署数量增加。 在过去的十年中&#xff0c;开发了诸如Docker和Kubernetes之类的容器化和编排工具&#xff0c;从而使微服务模式真正易于采用。 本文将教…

java考试安徽工业大学_2011~2012《Java语言程序设计》试卷A及答案(安徽工业大学)...

《2011~2012《Java语言程序设计》试卷A及答案(安徽工业大学)》由会员分享&#xff0c;可在线阅读&#xff0c;更多相关《2011~2012《Java语言程序设计》试卷A及答案(安徽工业大学)(6页珍藏版)》请在人人文库网上搜索。1、装 订 线 安 徽 工 业 大 学 试 题 纸(一)20112012学年第…

jsr303自定义验证_JSR 310新日期/时间API的自定义JSR 303 Bean验证约束

jsr303自定义验证借助JSR 310&#xff0c;Java 8终于为我们带来了不错的日期和时间API。 对于仍在使用Java 7的那些人&#xff08;就像我目前在我的当前项目中一样&#xff09;&#xff0c;有很好的反向移植&#xff0c;请访问www.threeten.org了解更多详细信息。 但是&#xf…

java spring注入 静态方法_JAVA静态方法中如何使用spring@Value进行注入的成员变量...

背景&#xff1a;一个旧项目原本集成有spring-session&#xff0c;现需要临时添加缓存的操作&#xff0c;需要复用spring-session中的一些缓存配置。实现方法&#xff1a;一、类上添加注解Component二、定义静态成员变量private static String redisUrl;private static String …

使用Testcontainers和PostgreSQL,MySQL或MariaDB的Spring Boot测试

Testcontainers是一个Java库&#xff0c;可轻松将Docker容器集成到JUnit测试中。 在Containerized World中 &#xff0c;将测试配置与嵌入式数据库和服务复杂化几乎没有意义。 而是使用在Docker中运行您的服务&#xff0c;并让Testcontainers为您管理此服务。 在此博客文章中&…

使用Spring Boot和Project Reactor处理SQS消息-第2部分

这是我关于使用Spring Boot和Project Reactor有效处理SQS消息的博客文章的后续文章 我在第一部分中列出了一些方法上的差距。 1.处理SQS客户端调用中的失败 2.该方法一次只能处理来自SQS的一条消息&#xff0c;如何并行化 3.它不处理错误&#xff0c;管道中的任何错误都会中…

java爬虫jsoup_Java爬虫之利用Jsoup自制简单的搜索引擎

内容导读在上述代码中&#xff0c;url为输入词条(暂时仅限于英文)&#xff0c;进入while循环可一直搜索&#xff0c;当输入为’exit’时退出。contentText为该词条的百度百科简介的网页形式&#xff0c;通过正则表达式将其中的文字提取出来。代码虽然简洁&#xff0c;但是功能还…