在此博客文章中,我想演示如何在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 Collection
allHeros() {
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
。
org.testcontainers
testcontainers
test
org.testcontainers
junit-jupiter
test
org.testcontainers
mysql
test
org.junit.jupiter
junit-jupiter
test
org.junit
junit-bom
${junit.jupiter.version}
pom
import
org.testcontainers
testcontainers-bom
${testcontainers.version}
pom
import
现在,我们拥有编写第一个测试的所有内容。
package com.github.sparsick.testcontainerspringboot.hero.universum;
import ...
@Testcontainers
class HeroClassicJDBCRepositoryIT {
@Container
private MySQLContainer database = new MySQLContainer();
private HeroClassicJDBCRepository repositoryUnderTest;
@Test
void testInteractionWithDatabase() {
ScriptUtils.runInitScript(new JdbcDatabaseDelegate(database, ""),"ddl.sql");
repositoryUnderTest = new HeroClassicJDBCRepository(dataSource());
repositoryUnderTest.addHero(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
Collection heroes = repositoryUnderTest.allHeros();
assertThat(heroes).hasSize(1);
}
@NotNull
private 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 failed
at 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 configuration
at 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 more
org.testcontainers.containers.ContainerLaunchException: Container startup failed
该错误消息表示Docker守护程序未运行。 确保Docker守护程序正在运行后,测试运行将成功。
控制台输出中有很多调试消息。 测试中的日志记录输出可以通过src/test/resources
的logback.xml
文件进行配置:
有关日志记录的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 {
@PersistenceContext
private EntityManager em;
@Transactional
public void addHero(Hero hero) {
em.persist(hero);
}
public Collection
allHeros() {
return em.createQuery("Select hero FROM Hero hero", Hero.class).getResultList();
}
public Collection
findHerosBySearchCriteria(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带来了一些测试支持类。 因此,我们必须向该项目添加进一步的测试依赖项。
org.springframework.boot
spring-boot-starter-test
test
该入门程序还包括JUnit Jupiter依赖关系和其他测试库的依赖关系,因此您可以根据需要从依赖关系声明中删除这些依赖关系。
现在,我们拥有编写测试的所有内容。
package com.github.sparsick.testcontainerspringboot.hero.universum;
import ...
@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = HeroClassicJpaRepositoryTest.Initializer.class)
class HeroClassicJpaRepositoryIT {
@Container
private static MySQLContainer database = new MySQLContainer();
@Autowired
private HeroClassicJpaRepository repositoryUnderTest;
@Test
void findHeroByCriteria(){
repositoryUnderTest.addHero(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
Collection
heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");
assertThat(heros).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
}
static class Initializer implements
ApplicationContextInitializer
{
public void initialize(ConfigurableApplicationContext
configurableApplicationContext) {
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 database
是static
,在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 {
@Query("SELECT hero FROM Hero hero where hero.city LIKE :searchCriteria OR hero.name LIKE :searchCriteria OR hero.universum = :searchCriteria")
List 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 {
@Container
private static MySQLContainer database = new MySQLContainer();
@Autowired
private HeroSpringDataJpaRepository repositoryUnderTest;
@Test
void findHerosBySearchCriteria() {
repositoryUnderTest.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
Collection heros = repositoryUnderTest.findHerosBySearchCriteria("Batman");
assertThat(heros).hasSize(1).contains(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
}
static class Initializer implements
ApplicationContextInitializer {
public void initialize(ConfigurableApplicationContext
configurableApplicationContext) {
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 implements
ApplicationContextInitializer
{
public void initialize(ConfigurableApplicationContext
configurableApplicationContext) {
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 {
@Autowired
private HeroSpringDataJpaRepository repositoryUnderTest;
@Test
void findHerosBySearchCriteria() {
repositoryUnderTest.save(new Hero("Batman", "Gotham City", ComicUniversum.DC_COMICS));
Collection 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 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 {
@Container
private static MySQLContainer database = new MySQLContainer();
@Autowired
private MockMvc mockMvc;
@Autowired
private HeroSpringDataJpaRepository heroRepository;
@Test
void 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 {
@Override
public 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模块编写测试。
您还有其他针对持久层编写测试的想法吗? 请让我知道并写评论。
更多的信息
- BOM“物料清单”的概念
- 测试容器
- Spring Boot文档–日志记录
- Spring Boot文档–自动配置的数据JPA测试
- 测试容器–单容器模式
- Spring Boot文档– MockMVC
- GitHub存储库中的完整示例
翻译自: https://www.javacodegeeks.com/2020/05/using-testcontainers-in-spring-boot-tests-for-database-integration-tests.html