在此博客文章中,我想演示如何在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

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

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

    Testcontainers是一个Java库,可轻松将Docker容器集成到JUnit测试中. 在Containerized World中 ,将测试配置与嵌入式数据库和服务复杂化几乎没有意义. 而是使 ...

  2. 在Spring Boot项目中使用Spock框架

    转载:https://www.jianshu.com/p/f1e354d382cd Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring B ...

  3. Spring Boot JPA中关联表的使用

    文章目录 添加依赖 构建Entity 构建Repository 构建初始数据 测试 Spring Boot JPA中关联表的使用 本文中,我们会将会通过一个Book和Category的关联关系,来讲解 ...

  4. spring boot测试_测试Spring Boot有条件的合理方式

    spring boot测试 如果您或多或少有经验的Spring Boot用户,那么很幸运,在某些时候您可能需要遇到必须有条件地注入特定bean或配置的情况 . 它的机制是很好理解的 ,但有时这样的测试 ...

  5. 使用JUnit 5进行Spring Boot测试

    JUnit 5 (JUnit Jupiter)已经存在了相当长的一段时间,并且配备了许多功能. 但令人意外JUnit 5它不是一个默认的测试库相关,当涉及到春节开机测试入门:它仍然是JUnit 4.1 ...

  6. Spring Boot框架中使用Jackson的处理总结

    1.前言 通常我们在使用Spring Boot框架时,如果没有特别指定接口的序列化类型,则会使用Spring Boot框架默认集成的Jackson框架进行处理,通过Jackson框架将服务端响应的数据 ...

  7. Spring Boot 生产中的 16 条最佳实践

    来源:www.e4developer.com/2018/08/06/ Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring ...

  8. Spring Boot 架构中的国际化支持实践—— Spring Boot 全球化解决方案

    背景 Spring Boot 主要通过 Maven 或 Gradle 这样的构建系统以继承方式添加依赖,同时继承了 Spring 框架中的优秀元素,减少了 Spring MVC 架构中的复杂配置,内置 ...

  9. Guava Cache本地缓存在 Spring Boot应用中的实践

    概述 在如今高并发的互联网应用中,缓存的地位举足轻重,对提升程序性能帮助不小.而 3.x开始的 Spring也引入了对 Cache的支持,那对于如今发展得如火如荼的 Spring Boot来说自然也是 ...

最新文章

  1. CentOS 7.3 安装MySQL--Java--Tomcat
  2. 【nodejs爬虫】使用async控制并发写一个小说爬虫
  3. HTML 怎么修改,怎么修改HTML
  4. mysql设置token有效期_记住我 token保存到数据库
  5. 数据结构--图 Graph
  6. 【快速安装Docker服务及Docker配置、Docker常用命令。】
  7. C++ char 类型:字符型和最小的整型
  8. CentOS/Ubuntu 下 MySQL 的安装
  9. 精彩编码 【进制转换】
  10. illustrator插件-常用功能开发-置入多页面PDF-js脚本开发-AI插件
  11. Win10访问不了Samba网络共享的原因以及解决办法
  12. php美颜滤镜,美颜滤镜的虚幻不如一支玻尿酸来的真实
  13. 2021年4月4日腾讯笔试
  14. 如何通俗理解Word2Vec
  15. vuepress-theme-reco + Github Actions 构建静态博客,部署到第三方服务器
  16. 【c语言】矩阵的创建
  17. TCP拥塞控制和宽容
  18. mongodb数据迁移设置方法
  19. c++中fabs()和abs()的区别
  20. 62套儿童行业响应式Html5儿童慈善机构网站模板儿童公益组织企业官网模板儿童慈善CSS模板下载婴儿树儿童健康食品整站模板html5网页静态模板Bootstrap扁平化网站源码css3手机seo自适响

热门文章

  1. codeforces1438 E.Yurii Can Do Everything
  2. 【树链剖分】染色(luogu 2486/金牌导航 树链剖分-3)
  3. 面试经历—广州YY(欢聚时代)
  4. 接口 Closeable
  5. ssh(Spring+Spring mvc+hibernate)——EmpServiceImpl.java
  6. ArrayList基操
  7. vue项目没有启动成功的原因之一
  8. JAVA基础学习大全(笔记)
  9. 配置spring、SpringMVC,mybatis进行整合
  10. Meaven的pom文件配置