简介
在这篇文章中,我们将看到如何使用Testcontainers进行数据库集成测试。
如果你还在使用HSQLDB或H2来测试你的Spring Boot应用程序,这些应用程序在生产中运行于Oracle、SQL Server、PostgreSQL或MySQL,那么你最好改用Testcontainers。
内存数据库与Docker的对比
内存关系型数据库系统,如HSQLDB和H2是在2000年初开发的,有两个原因。
- 因为在当时,安装数据库系统是一个非常繁琐的过程,需要花费大量的时间。
- 与需要从磁盘加载或刷新到磁盘的数据库相比,内存数据库的访问速度要快好几个数量级。
然而,在2013年,Docker发布了,使得运行一个Docker容器变得非常容易,该容器可以在任何主机操作系统上托管一个关系数据库。
为了加快测试的数据访问速度,你可以将数据文件夹和交易日志挂载到tmpfs内存Linux存储中。
所以,如今我们有了更好的工具,而这些工具在2013年之前我们是没有的。我们现在可以使用Docker进行集成测试,而不是使用内存数据库。
Docker数据库容器的优势在于,你要针对你在生产中使用的同一个数据库版本进行测试。如果你有SQL特定的查询或存储过程,那么你不能使用H2或HSQLDB来测试它们。你需要一个真正的数据库引擎来运行特定的查询、存储过程或函数。
Testcontainers数据库集成测试
虽然你可以手动或使用docker-maven-plugin自动启动和停止Docker数据库容器,但有一个更好的方法。
Tectcontainers是一个开源项目,它提供了一个Java API,这样你就可以以编程方式管理你的Docker容器,这非常有用,你很快就会看到。
而向你展示Testcontainers如何工作的最好方式是将其整合到令人惊叹的Hibernate Types项目中。
Hibernate Types项目提供了对JSON, ARRAY, Ranges,CurrencyUnit,MonetaryAmount,YearMonth, 以及许多其他Oracle, SQL Server, PostgreSQL, 和MySQL的类型的支持。
为了增加对一个新类型的支持,你必须在所有这些关系数据库系统上运行集成测试。
因此,你有几个选择。
- 你可以在本地安装Oracle XE、SQL Server Express、PostgreSQL和MySQL
- 你可以使用Docker Compose来启动所有这些基于compose配置文件的数据库
- 你可以使用Testcontainers来自动完成这个过程。
在我的例子中,我已经在我的机器上安装了所有这些四大关系型数据库,因为我在高性能Java持久性培训和高性能SQL培训中大量使用它们,并开发超持久优化器。
但是,其他Hibernate Types项目的参与者可能没有安装所有这些数据库,所以运行项目集成测试对他们来说是个问题。
而这正是Testcontainers来拯救的地方!
你可以设置DataSource 提供者连接到预先定义的数据库URL,如果失败,就按需启动Docker容器。
向Hibernate类型添加Testcontainers
在使用Testcontainer之前,Hibernate Types已经使用了一个DataSourceProvider 抽象,允许测试定义所需的数据库,其外观如下。

DataSourceProvider 接口定义了数据库管理合同。
public interface DataSourceProvider {
Database database();
String hibernateDialect();
DataSource dataSource();
String url();
String username();
String password();
}
而AbstractContainerDataSourceProvider 定义了Testcontainers Docker数据库容器如何按需启动。
public abstract class AbstractContainerDataSourceProvider
implements DataSourceProvider {
@Override
public DataSource dataSource() {
DataSource dataSource = newDataSource();
try(Connection connection = dataSource.getConnection()) {
return dataSource;
} catch (SQLException e) {
Database database = database();
if(database.getContainer() == null) {
database.initContainer(username(), password());
}
return newDataSource();
}
}
@Override
public String url() {
JdbcDatabaseContainer container = database().getContainer();
return container != null ?
container.getJdbcUrl() :
defaultJdbcUrl();
}
protected abstract String defaultJdbcUrl();
protected abstract DataSource newDataSource();
}
AbstractContainerDataSourceProvider 允许我们使用一个本地数据库,这个数据库首先被解析。如果没有本地数据库可供连接,则通过调用相关Database 对象的initContainer 方法来实例化一个新的数据库容器,该方法看起来如下。
public enum Database {
POSTGRESQL {
@Override
protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
return new PostgreSQLContainer(
"postgres:13.7"
);
}
},
ORACLE {
@Override
protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
return new OracleContainer(
"gvenzl/oracle-xe:21.3.0-slim"
);
}
@Override
protected boolean supportsDatabaseName() {
return false;
}
},
MYSQL {
@Override
protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
return new MySQLContainer(
"mysql:8.0"
);
}
},
SQLSERVER {
@Override
protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
return new MSSQLServerContainer(
"mcr.microsoft.com/mssql/server:2019-latest"
);
}
@Override
protected boolean supportsDatabaseName() {
return false;
}
@Override
protected boolean supportsCredentials() {
return false;
}
};
private JdbcDatabaseContainer container;
public JdbcDatabaseContainer getContainer() {
return container;
}
public void initContainer(String username, String password) {
container = (JdbcDatabaseContainer) newJdbcDatabaseContainer()
.withEnv(Collections.singletonMap("ACCEPT_EULA", "Y"))
.withTmpFs(Collections.singletonMap("/testtmpfs", "rw"));
if(supportsDatabaseName()) {
container.withDatabaseName("high-performance-java-persistence");
}
if(supportsCredentials()) {
container.withUsername(username).withPassword(password);
}
container.start();
}
protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
throw new UnsupportedOperationException(
String.format(
"The [%s] database was not configured to use Testcontainers!",
name()
)
);
}
protected boolean supportsDatabaseName() {
return true;
}
protected boolean supportsCredentials() {
return true;
}
}
特定于数据库的DataSource 实例化逻辑被定义在DataSourceProvider 实现中。
public class MySQLDataSourceProvider
extends AbstractContainerDataSourceProvider {
@Override
public String hibernateDialect() {
return "org.hibernate.dialect.MySQL8Dialect";
}
@Override
protected String defaultJdbcUrl() {
return "jdbc:mysql://localhost/high_performance_java_persistence?useSSL=false";
}
protected DataSource newDataSource() {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL(url());
dataSource.setUser(username());
dataSource.setPassword(password());
return dataSource;
}
@Override
public String username() {
return "mysql";
}
@Override
public String password() {
return "admin";
}
@Override
public Database database() {
return Database.MYSQL;
}
}
这就是了!
如果我停止我的本地MySQL数据库并运行一个MySQL测试,我可以看到一个MySQL容器是按需启动的。
DEBUG [main]: ?.0] - Trying to create JDBC connection using com.mysql.cj.jdbc.Driver to
jdbc:mysql://localhost:57127/high-performance-java-persistence?useSSL=false&allowPublicKeyRetrieval=true
with properties: {
user=mysql,
password=admin
}
INFO [main]: ?.0] - Container is started (
JDBC URL: jdbc:mysql://localhost:57127/high-performance-java-persistence
)
INFO [main]: ?.0] - Container mysql:8.0 started in PT34.213S
总结
在Hibernate Types项目中添加对Testcontainers的支持是一个非常简单的过程。然而,其优势是巨大的,因为现在任何人都可以运行现有的测试案例,而不需要事先进行数据库配置。
虽然你可以使用Docker来管理你的容器,但使用Testcontainers以编程方式来做要方便得多。在我的案例中,Testcontainers允许我只在没有可用的本地数据库时启动Docker容器。